diff --git a/src/features/agents/Session.module.css b/src/features/agents/Session.module.css
index 69b301e12..db8f63a0d 100644
--- a/src/features/agents/Session.module.css
+++ b/src/features/agents/Session.module.css
@@ -96,7 +96,7 @@
cursor: default;
}
-/* The conversation plus what floats over it (find bar, Quote, Jump to Bottom). */
+/* The conversation plus what floats over it (find bar, Jump to Bottom). */
.streamArea {
position: relative;
flex: 1;
@@ -167,24 +167,6 @@
box-shadow: var(--shadow-tooltip);
}
-.quoteButton {
- position: absolute;
- z-index: 2;
- height: 24px;
- padding: 0 10px;
- border: 1px solid var(--border);
- border-radius: var(--radius-small);
- background: var(--bg-popup);
- color: var(--fg);
- box-shadow: var(--shadow-tooltip);
- cursor: pointer;
- -webkit-user-select: none;
- user-select: none;
-}
-.quoteButton:hover {
- background: var(--bg-hover);
-}
-
.findBar {
flex: none;
display: flex;
diff --git a/src/features/agents/SessionEditor.tsx b/src/features/agents/SessionEditor.tsx
index 3d137a1b9..d7fe4cac6 100644
--- a/src/features/agents/SessionEditor.tsx
+++ b/src/features/agents/SessionEditor.tsx
@@ -1,5 +1,5 @@
/** An agent task as an editor tab: the conversation, a composer and the task's changed files. */
-import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type RefObject } from "react";
+import { useEffect, useLayoutEffect, useMemo, useRef, useState, type RefObject } from "react";
import clsx from "clsx";
import type { AgentChange, AgentEntry, AgentTask } from "@/bindings";
import { executeAction } from "@/platform/actions";
@@ -15,9 +15,11 @@ import {
Spinner,
T,
Tree,
+ copyMenuFor,
copyText,
fileIconName,
selectableText,
+ showContextMenu,
} from "@/ui";
import { useTx } from "@/platform/i18n";
import { TaskStatusIcon } from "./AgentsView";
@@ -191,6 +193,17 @@ function Stream({ task, entries, finding, findFocus, onCloseFind }: StreamProps)
stick.current = el.scrollHeight - el.scrollTop - el.clientHeight < 40;
setAtBottom(stick.current);
}}
+ // Copy, plus Quote for a selection: replies to it in the composer.
+ onContextMenu={(e) => {
+ const text = selectionIn(e.currentTarget);
+ const items = copyMenuFor(e.target) ?? [];
+ if (text.trim() && task.status !== "archived") {
+ items.push({ id: "quote", label: "Quote", icon: "edit", run: () => quoteInComposer(task.id, text) });
+ }
+ if (!items.length) return;
+ e.preventDefault();
+ showContextMenu(e, items);
+ }}
>
{entries === null ? (
@@ -205,7 +218,6 @@ function Stream({ task, entries, finding, findFocus, onCloseFind }: StreamProps)
)}
-
{!atBottom && (
{formatDuration(now - since)};
}
-/** A Quote button next to text selected in the conversation: replies to it in the composer. */
-function QuoteSelection({ task, streamRef }: { task: AgentTask; streamRef: RefObject }) {
- const [at, setAt] = useState<{ left: number; top: number } | null>(null);
- const place = useCallback(() => {
- const stream = streamRef.current;
- const sel = window.getSelection();
- if (!stream || !sel || sel.isCollapsed || !sel.rangeCount || !sel.toString().trim()) return setAt(null);
- const range = sel.getRangeAt(0);
- if (!stream.contains(range.commonAncestorContainer)) return setAt(null);
- const rects = range.getClientRects();
- const last = rects[rects.length - 1] ?? range.getBoundingClientRect();
- const area = stream.parentElement!.getBoundingClientRect();
- const box = stream.getBoundingClientRect();
- if (last.bottom < box.top || last.top > box.bottom) return setAt(null);
- setAt({
- left: Math.min(Math.max(last.right - area.left, 0), area.width - 80),
- top: Math.min(last.bottom - area.top + 4, box.bottom - area.top - 28),
- });
- }, [streamRef]);
- useEffect(() => {
- const stream = streamRef.current;
- document.addEventListener("selectionchange", place);
- stream?.addEventListener("scroll", place);
- return () => {
- document.removeEventListener("selectionchange", place);
- stream?.removeEventListener("scroll", place);
- };
- }, [place, streamRef]);
- if (!at || task.status === "archived") return null;
- return (
-
- );
+/** Text selected in `stream`, or "" when the selection is elsewhere. */
+function selectionIn(stream: HTMLElement): string {
+ const sel = window.getSelection();
+ if (!sel || sel.isCollapsed || !sel.rangeCount) return "";
+ return stream.contains(sel.getRangeAt(0).commonAncestorContainer) ? sel.toString() : "";
}
const highlights = (globalThis as { CSS?: { highlights?: Map } }).CSS?.highlights;