Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
# APP-1915: Copy URL / Copy path in AI response right-click context menu
|
||||
|
||||
## Summary
|
||||
|
||||
When a user right-clicks a hyperlink rendered inside an AI response, add a "Copy URL" (for web URLs) or "Copy path" (for file paths) item to the existing AI block context menu, grouped with the other Copy items. The full AI block context menu must still be shown — the link-specific item is an addition, not a replacement.
|
||||
|
||||
## Problem
|
||||
|
||||
AI responses often contain links (URLs and file paths). Today there is no quick way to copy a link target from the context menu; users have to manually select the link text. Terminal grid links already offer "Copy URL" / "Copy path" on right-click, so AI responses are an outlier.
|
||||
|
||||
A previous change on `oz-agent/copy-url-in-ai-response-context-menu` added the affordance but replaced the entire AI block right-click context menu with a one-item "Copy URL" menu, regressing every other right-click action (Share session, Copy, Copy prompt, Copy output as Markdown, Save as prompt, Share conversation, Fork…, Rewind…, Copy debugging link/ID, Split pane…, Close pane) whenever the cursor happened to be over a link. That is the bug this spec addresses.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Adding "Show in Finder", "Open in Warp", or "Open in editor" items for file-path links in AI responses. Scope is limited to copying the target.
|
||||
- Changing the terminal grid link context menu.
|
||||
- Adding a separate slimmed-down link-only menu (the earlier attempt). The existing AI block menu is kept intact.
|
||||
- Changing how hyperlinks are detected or rendered inside AI responses.
|
||||
|
||||
## Figma
|
||||
|
||||
Figma: none provided. The existing AI block context menu (see screenshots attached to APP-1915) is the baseline; the only visible change is an additional "Copy URL" or "Copy path" item inserted next to the other Copy items.
|
||||
|
||||
## Behavior
|
||||
|
||||
1. Right-clicking a URL hyperlink inside an AI response shows the full existing AI block context menu (Share session, Copy, Copy prompt, Copy output as Markdown, Save as prompt, Share conversation, Copy conversation text, Fork…, Rewind…, Copy debugging link, Copy conversation ID, Split pane…, Close pane) with no items removed and no other items reordered.
|
||||
|
||||
2. When the cursor is over a URL hyperlink, a "Copy URL" item is inserted into the menu immediately after "Copy output as Markdown" and before any conditional "Copy command" / "Copy git branch" items. Selecting it writes the hovered URL string verbatim to the clipboard (the URL target, not the displayed link text).
|
||||
|
||||
3. When the cursor is over a file-path hyperlink, a "Copy path" item is inserted in the same position as "Copy URL" (immediately after "Copy output as Markdown"). Selecting it writes the absolute path of the hovered file to the clipboard.
|
||||
|
||||
4. "Copy path" is only available on builds that have the `local_fs` feature enabled, matching the existing file-path link behavior elsewhere in the app. On builds without `local_fs`, no "Copy path" item appears and the rest of the menu is unchanged.
|
||||
|
||||
5. At most one link-specific item is inserted per menu: "Copy URL" xor "Copy path", never both, and never duplicated for overlapping link regions.
|
||||
|
||||
6. The order of Copy items within the AI block menu is stable: Copy → Copy prompt → Copy output as Markdown → (Copy URL or Copy path, when on a link) → Copy command (when applicable) → Copy git branch (when applicable). Non-Copy items retain their existing relative order.
|
||||
|
||||
7. Right-clicking anywhere inside an AI response where the cursor is not on a hyperlink shows the existing AI block context menu unchanged — no link-specific items, no reorderings, no omissions.
|
||||
|
||||
8. Right-clicking inside an AI response while a text selection is active shows the existing selection-oriented menu (Copy, Insert into input, optionally Ask Warp AI / Attach as agent mode context). No "Copy URL" or "Copy path" item is added in this case, even if the selection overlaps a link — the user's primary intent is the selection.
|
||||
|
||||
9. Right-clicking a link in the terminal grid (outside AI responses) is unchanged: it continues to show the existing grid link context menu (Copy URL / Copy path / Show in Finder / Open in Warp / Open in editor).
|
||||
|
||||
10. Right-click never crashes or panics when there is no hovered link at the time the menu is requested; the link-specific item is simply omitted.
|
||||
|
||||
11. The link-specific item is computed from the hover state at the moment the menu is opened. If the hovered link changes or disappears while the menu is open, the already-shown menu is not mutated; the next right-click recomputes from the new hover state.
|
||||
@@ -0,0 +1,119 @@
|
||||
# APP-1915: Tech Spec
|
||||
|
||||
## Context
|
||||
|
||||
See `PRODUCT.md` for user-visible behavior. The feature branch was rewritten before this spec was finalized, so the diff against `master` is additive only — it introduces the hover-aware plumbing rather than removing a buggy short-circuit.
|
||||
|
||||
Implementation anchors in the current code:
|
||||
|
||||
- `app/src/ai/blocklist/block.rs (3910-3936)` — new `AIBlock::hovered_rich_content_link`. Reads `detected_links_state.currently_hovered_link_location` and maps the underlying `DetectedLinkType` into a `RichContentLink` (`Url` or, under `#[cfg(feature = "local_fs")]`, `FilePath { absolute_path, line_and_column_num, target_override }`).
|
||||
- `app/src/terminal/view.rs:14252` — new `TerminalView::hovered_rich_content_link_for_view`, a thin wrapper that resolves the `EntityId` back to the `AIBlock` handle and delegates to `AIBlock::hovered_rich_content_link`.
|
||||
- `app/src/terminal/view.rs (14262-14803)` — `context_menu_items`. The existing top match at `(14269-14340)` continues to handle the terminal grid `highlighted_link` path unchanged. The `RichContentBlockRightClick` branch at `(14712-14803)` is the AI block path; this is where the hovered link is computed and threaded through.
|
||||
- `app/src/terminal/view.rs (15514-15655)` — `ai_block_copying_menu_items`, which builds the Copy group (Copy → Copy prompt → Copy output as Markdown → conditional Copy command / Copy git branch → Save as prompt → Share conversation → Copy conversation text). The link-specific item is inserted immediately after "Copy output as Markdown" and before the conditional Copy command / git branch items.
|
||||
- `RichContentLink` enum (in `view.rs`): `Url(String)` and `#[cfg(feature = "local_fs")] FilePath { absolute_path, line_and_column_num, target_override }`. `ContextMenuAction::CopyUrl { url_content }` is reused for both variants — for `FilePath`, the absolute path is copied via `to_string_lossy().into_owned()`.
|
||||
|
||||
There are two callers of `ai_block_copying_menu_items` in `view.rs`, both updated to accept the new `Option<RichContentLink>` parameter:
|
||||
|
||||
- `view.rs:14723` — right-click on an AI block (`BlockListMenuSource::RichContentBlockRightClick`). Passes the computed `Some(link)` when the cursor is over a hyperlink, `None` otherwise.
|
||||
- `view.rs:15716` — `open_ai_block_overflow_context_menu`, triggered by the three-dot overflow button on an AI block. This surface has no hovered-link concept, so it always passes `None`.
|
||||
|
||||
`RichContentTextRightClick` (selection-active right-click in an AI block) intentionally does not participate: it is handled by a different arm in `context_menu_items` and builds the selection-oriented menu, per `PRODUCT.md` Behavior 8.
|
||||
|
||||
## Proposed changes
|
||||
|
||||
1. **Add `AIBlock::hovered_rich_content_link`** (`app/src/ai/blocklist/block.rs`). Returns `Option<RichContentLink>` by reading the already-maintained `detected_links_state.currently_hovered_link_location` and mapping the underlying `DetectedLinkType` into the `RichContentLink` variants the terminal view already understands.
|
||||
|
||||
2. **Add `TerminalView::hovered_rich_content_link_for_view`** (`app/src/terminal/view.rs`). Resolves the `EntityId` to the AI block handle via the existing `ai_block_handle_by_view_id` helper and delegates to `AIBlock::hovered_rich_content_link`.
|
||||
|
||||
3. **Add an `Option<RichContentLink>` parameter to `ai_block_copying_menu_items`.** When `Some`, push exactly one additional `MenuItem` immediately after "Copy output as Markdown" (before the conditional "Copy command" / "Copy git branch"):
|
||||
|
||||
```rust path=null start=null
|
||||
if let Some(link) = hovered_link {
|
||||
match link {
|
||||
RichContentLink::Url(url) => items.push(
|
||||
MenuItemFields::new("Copy URL")
|
||||
.with_on_select_action(TerminalAction::ContextMenu(
|
||||
ContextMenuAction::CopyUrl { url_content: url },
|
||||
))
|
||||
.into_item(),
|
||||
),
|
||||
#[cfg(feature = "local_fs")]
|
||||
RichContentLink::FilePath { absolute_path, .. } => items.push(
|
||||
MenuItemFields::new("Copy path")
|
||||
.with_on_select_action(TerminalAction::ContextMenu(
|
||||
ContextMenuAction::CopyUrl {
|
||||
url_content: absolute_path.to_string_lossy().into_owned(),
|
||||
},
|
||||
))
|
||||
.into_item(),
|
||||
),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. **Update both callers of `ai_block_copying_menu_items`:**
|
||||
|
||||
- `view.rs:14723` in the `RichContentBlockRightClick` branch — compute the hovered link once and pass it through:
|
||||
|
||||
```rust path=null start=null
|
||||
let hovered_link = self.hovered_rich_content_link_for_view(*rich_content_view_id, ctx);
|
||||
items.extend(self.ai_block_copying_menu_items(
|
||||
*rich_content_view_id,
|
||||
ai_metadata.conversation_id,
|
||||
hovered_link.clone(),
|
||||
&model,
|
||||
ctx,
|
||||
));
|
||||
```
|
||||
|
||||
- `view.rs:15716` in `open_ai_block_overflow_context_menu` — always pass `None` (the overflow button has no hover context).
|
||||
|
||||
5. **Intentionally skip `RichContentTextRightClick`.** That branch fires only when a text selection is active (see `block_list_element.rs (1417-1428)`) and `PRODUCT.md` Behavior 8 keeps the selection-oriented menu unchanged.
|
||||
|
||||
## End-to-end flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant BlockList as BlockListElement
|
||||
participant View as TerminalView
|
||||
participant AIBlock as AIBlock
|
||||
participant Clipboard
|
||||
|
||||
User->>BlockList: Right-click on URL inside AI response
|
||||
BlockList->>View: BlockListMenuSource::RichContentBlockRightClick
|
||||
View->>View: hovered_rich_content_link_for_view()
|
||||
View->>AIBlock: hovered_rich_content_link()
|
||||
AIBlock-->>View: Some(RichContentLink::Url(url))
|
||||
View->>View: ai_block_copying_menu_items(..., Some(link), ...)
|
||||
Note over View: Inserts "Copy URL" after "Copy output as Markdown"
|
||||
View-->>User: Full AI block menu + Copy URL
|
||||
User->>View: Click "Copy URL"
|
||||
View->>Clipboard: Write url
|
||||
```
|
||||
|
||||
## Risks and mitigations
|
||||
|
||||
1. **Menu ordering regressions.** Insertion is strictly after "Copy output as Markdown" and before "Copy command" / "Copy git branch"; `PRODUCT.md` Behavior 6 pins the order. Manual validation confirms it.
|
||||
2. **`local_fs` feature gating.** The "Copy path" branch stays behind `#[cfg(feature = "local_fs")]` to match the existing `RichContentLink::FilePath` variant. Covered by `PRODUCT.md` Behavior 4.
|
||||
3. **Selection path.** `RichContentTextRightClick` does not receive the new item. Intentional per `PRODUCT.md` Behavior 8; a link-specific path during selection can be added as a follow-up if needed.
|
||||
|
||||
## Testing and validation
|
||||
|
||||
Each `PRODUCT.md` Behavior invariant maps to a concrete verification step:
|
||||
|
||||
- Behavior 1, 2, 6: Manual — open an AI response with a URL list (similar to the APP-1915 screenshot), right-click a URL, confirm the full AI block menu is shown with "Copy URL" inserted immediately after "Copy output as Markdown" and before any "Copy command" / "Copy git branch". Click it and confirm the clipboard contains the URL verbatim.
|
||||
- Behavior 3, 4: Manual on a `local_fs` build — right-click a file-path link in an AI response, confirm "Copy path" is in the same position and copies the absolute path. On a non-`local_fs` build, confirm no "Copy path" item appears and the rest of the menu is unchanged.
|
||||
- Behavior 5: Manual — hover a link, right-click, confirm the link-specific item appears exactly once and that "Copy URL" and "Copy path" never appear together.
|
||||
- Behavior 7: Manual — right-click in an AI response body away from any link; menu matches the pre-regression baseline with no link-specific item.
|
||||
- Behavior 8: Manual — with a text selection inside an AI response, right-click and confirm the selection-oriented menu is unchanged.
|
||||
- Behavior 9: Manual — right-click a URL in the terminal grid; grid link menu is unchanged.
|
||||
- Behavior 10: The new conditional push is guarded by `Option::Some`, so a missing hovered link cannot panic. A small regression test in `view_test.rs` that asserts "Copy URL" is present when a hovered URL link is set — and absent otherwise — is recommended alongside the manual checks.
|
||||
- Behavior 11: Implicitly covered — menu items are built from hover state at the moment the menu is opened; no mutation of an already-open menu.
|
||||
|
||||
Existing `view_test.rs` coverage for `RichContentBlockRightClick` must continue to pass.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- Optional: add "Open link" or "Open in editor" items for file paths in AI responses, to reach parity with the grid link menu.
|
||||
- Optional: add a hovered-link path for `RichContentTextRightClick` if user feedback asks for it.
|
||||
Reference in New Issue
Block a user