authorgravatar for Taras.Fomin@gmail.comTaras <Taras.Fomin@gmail.com> 2026-07-18 22:05:09+00:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-19 00:05:09+02:00
log1735e27d9e5106bbb35d5b1dd10363604a54b69e
tree3ccd6632b52ebe00aba49d31977fb633bd229551
parentebe8afb1df357423a0e036b388af3e739d640205
signature Signed by PGP key B5690EEEBB952194

Add terminal selection copy action (#2904)


1 files changed, 43 insertions(+), 14 deletions(-)

apps/web/src/components/ThreadTerminalDrawer.tsx+43-14
...@@ -31,6 +31,7 @@ import {...@@ -31,6 +31,7 @@ import {
31 useState,31 useState,
32} from "react";32} from "react";
33import { Popover, PopoverPopup, PopoverTrigger } from "~/components/ui/popover";33import { Popover, PopoverPopup, PopoverTrigger } from "~/components/ui/popover";
34import { writeTextToClipboard } from "~/hooks/useCopyToClipboard";
34import { cn } from "~/lib/utils";35import { cn } from "~/lib/utils";
35import { type TerminalContextSelection } from "~/lib/terminalContext";36import { type TerminalContextSelection } from "~/lib/terminalContext";
36import { useOpenInPreferredEditor } from "../editorPreferences";37import { useOpenInPreferredEditor } from "../editorPreferences";
...@@ -330,7 +331,7 @@ export function TerminalViewport({...@@ -330,7 +331,7 @@ export function TerminalViewport({
330 const selectionPointerRef = useRef<{ x: number; y: number } | null>(null);331 const selectionPointerRef = useRef<{ x: number; y: number } | null>(null);
331 const selectionGestureActiveRef = useRef(false);332 const selectionGestureActiveRef = useRef(false);
332 const selectionActionRequestIdRef = useRef(0);333 const selectionActionRequestIdRef = useRef(0);
333 const selectionActionOpenRef = useRef(false);334 const selectionActionMenuOpenRef = useRef(false);
334 const selectionActionTimerRef = useRef<number | null>(null);335 const selectionActionTimerRef = useRef<number | null>(null);
335 const keybindingsRef = useRef(keybindings);336 const keybindingsRef = useRef(keybindings);
336 const runtimeEnvKey = useMemo(() => runtimeEnvSignature(runtimeEnv), [runtimeEnv]);337 const runtimeEnvKey = useMemo(() => runtimeEnvSignature(runtimeEnv), [runtimeEnv]);
...@@ -417,6 +418,7 @@ export function TerminalViewport({...@@ -417,6 +418,7 @@ export function TerminalViewport({
417418
418 const readSelectionAction = (): {419 const readSelectionAction = (): {
419 position: { x: number; y: number };420 position: { x: number; y: number };
421 clipboardText: string;
420 selection: TerminalContextSelection;422 selection: TerminalContextSelection;
421 } | null => {423 } | null => {
422 const activeTerminal = terminalRef.current;424 const activeTerminal = terminalRef.current;
...@@ -445,6 +447,7 @@ export function TerminalViewport({...@@ -445,6 +447,7 @@ export function TerminalViewport({
445 });447 });
446 return {448 return {
447 position,449 position,
450 clipboardText: selectionText,
448 selection: {451 selection: {
449 terminalId,452 terminalId,
450 terminalLabel: readTerminalLabel(),453 terminalLabel: readTerminalLabel(),
...@@ -460,7 +463,7 @@ export function TerminalViewport({...@@ -460,7 +463,7 @@ export function TerminalViewport({
460 clearSelectionAction();463 clearSelectionAction();
461 return;464 return;
462 }465 }
463 if (selectionActionOpenRef.current) {466 if (selectionActionMenuOpenRef.current) {
464 return;467 return;
465 }468 }
466 const nextAction = readSelectionAction();469 const nextAction = readSelectionAction();
...@@ -469,20 +472,46 @@ export function TerminalViewport({...@@ -469,20 +472,46 @@ export function TerminalViewport({
469 return;472 return;
470 }473 }
471 const requestId = ++selectionActionRequestIdRef.current;474 const requestId = ++selectionActionRequestIdRef.current;
472 selectionActionOpenRef.current = true;475 selectionActionMenuOpenRef.current = true;
473 try {476 const clicked = await localApi.contextMenu
474 const clicked = await localApi.contextMenu.show(477 .show(
475 [{ id: "add-to-chat", label: "Add to chat" }],478 [
479 { id: "add-to-chat", label: "Add to chat" },
480 { id: "copy", label: "Copy" },
481 ],
476 nextAction.position,482 nextAction.position,
477 );483 )
478 if (requestId !== selectionActionRequestIdRef.current || clicked !== "add-to-chat") {484 .finally(() => {
485 selectionActionMenuOpenRef.current = false;
486 });
487 if (requestId !== selectionActionRequestIdRef.current || clicked === null) {
488 return;
489 }
490 switch (clicked) {
491 case "add-to-chat":
492 handleAddTerminalContext(nextAction.selection);
493 terminalRef.current?.clearSelection();
494 terminalRef.current?.focus();
495 return;
496 case "copy":
497 try {
498 await writeTextToClipboard(nextAction.clipboardText, "terminal selection");
499 } catch (error) {
500 if (requestId !== selectionActionRequestIdRef.current) {
501 return;
502 }
503 const activeTerminal = terminalRef.current;
504 if (activeTerminal) {
505 writeSystemMessage(
506 activeTerminal,
507 error instanceof Error ? error.message : "Unable to copy terminal selection",
508 );
509 }
510 }
511 if (requestId === selectionActionRequestIdRef.current) {
512 terminalRef.current?.focus();
513 }
479 return;514 return;
480 }
481 handleAddTerminalContext(nextAction.selection);
482 terminalRef.current?.clearSelection();
483 terminalRef.current?.focus();
484 } finally {
485 selectionActionOpenRef.current = false;
486 }515 }
487 };516 };
488517