|
9 | 9 | XIcon, |
10 | 10 | } from "lucide-react"; |
11 | 11 | import { |
| 12 | + type ContextMenuItem, |
12 | 13 | type ResolvedKeybindingsConfig, |
13 | 14 | type ScopedThreadRef, |
14 | 15 | type TerminalAttachStreamEvent, |
@@ -66,6 +67,14 @@ const MIN_DRAWER_HEIGHT = 180; |
66 | 67 | const MAX_DRAWER_HEIGHT_RATIO = 0.75; |
67 | 68 | const MULTI_CLICK_SELECTION_ACTION_DELAY_MS = 260; |
68 | 69 |
|
| 70 | +type TerminalSelectionActionId = "add-to-chat" | "copy"; |
| 71 | + |
| 72 | +export const TERMINAL_SELECTION_ACTION_MENU_ITEMS: readonly ContextMenuItem<TerminalSelectionActionId>[] = |
| 73 | + [ |
| 74 | + { id: "add-to-chat", label: "Add to chat" }, |
| 75 | + { id: "copy", label: "Copy" }, |
| 76 | + ]; |
| 77 | + |
69 | 78 | function maxDrawerHeight(): number { |
70 | 79 | if (typeof window === "undefined") return DEFAULT_THREAD_TERMINAL_HEIGHT; |
71 | 80 | return Math.max(MIN_DRAWER_HEIGHT, Math.floor(window.innerHeight * MAX_DRAWER_HEIGHT_RATIO)); |
@@ -264,6 +273,13 @@ export function shouldHandleTerminalSelectionMouseUp( |
264 | 273 | return selectionGestureActive && button === 0; |
265 | 274 | } |
266 | 275 |
|
| 276 | +export async function copyTerminalSelectionTextToClipboard(text: string): Promise<void> { |
| 277 | + if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) { |
| 278 | + throw new Error("Clipboard API unavailable."); |
| 279 | + } |
| 280 | + await navigator.clipboard.writeText(text); |
| 281 | +} |
| 282 | + |
267 | 283 | interface TerminalViewportProps { |
268 | 284 | threadRef: ScopedThreadRef; |
269 | 285 | threadId: ThreadId; |
@@ -418,15 +434,33 @@ export function TerminalViewport({ |
418 | 434 | selectionActionOpenRef.current = true; |
419 | 435 | try { |
420 | 436 | const clicked = await localApi.contextMenu.show( |
421 | | - [{ id: "add-to-chat", label: "Add to chat" }], |
| 437 | + TERMINAL_SELECTION_ACTION_MENU_ITEMS, |
422 | 438 | nextAction.position, |
423 | 439 | ); |
424 | | - if (requestId !== selectionActionRequestIdRef.current || clicked !== "add-to-chat") { |
| 440 | + if (requestId !== selectionActionRequestIdRef.current || clicked === null) { |
425 | 441 | return; |
426 | 442 | } |
427 | | - handleAddTerminalContext(nextAction.selection); |
428 | | - terminalRef.current?.clearSelection(); |
429 | | - terminalRef.current?.focus(); |
| 443 | + switch (clicked) { |
| 444 | + case "add-to-chat": |
| 445 | + handleAddTerminalContext(nextAction.selection); |
| 446 | + terminalRef.current?.clearSelection(); |
| 447 | + terminalRef.current?.focus(); |
| 448 | + return; |
| 449 | + case "copy": |
| 450 | + try { |
| 451 | + await copyTerminalSelectionTextToClipboard(nextAction.selection.text); |
| 452 | + } catch (error) { |
| 453 | + const activeTerminal = terminalRef.current; |
| 454 | + if (activeTerminal) { |
| 455 | + writeSystemMessage( |
| 456 | + activeTerminal, |
| 457 | + error instanceof Error ? error.message : "Unable to copy terminal selection", |
| 458 | + ); |
| 459 | + } |
| 460 | + } |
| 461 | + terminalRef.current?.focus(); |
| 462 | + return; |
| 463 | + } |
430 | 464 | } finally { |
431 | 465 | selectionActionOpenRef.current = false; |
432 | 466 | } |
|
0 commit comments