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 {
3131 useState,
3232} from "react";
3333import { Popover, PopoverPopup, PopoverTrigger } from "~/components/ui/popover";
34import { writeTextToClipboard } from "~/hooks/useCopyToClipboard";
3435import { cn } from "~/lib/utils";
3536import { type TerminalContextSelection } from "~/lib/terminalContext";
3637import { useOpenInPreferredEditor } from "../editorPreferences";
......@@ -330,7 +331,7 @@ export function TerminalViewport({
330331 const selectionPointerRef = useRef<{ x: number; y: number } | null>(null);
331332 const selectionGestureActiveRef = useRef(false);
332333 const selectionActionRequestIdRef = useRef(0);
333 const selectionActionOpenRef = useRef(false);
334 const selectionActionMenuOpenRef = useRef(false);
334335 const selectionActionTimerRef = useRef<number | null>(null);
335336 const keybindingsRef = useRef(keybindings);
336337 const runtimeEnvKey = useMemo(() => runtimeEnvSignature(runtimeEnv), [runtimeEnv]);
......@@ -417,6 +418,7 @@ export function TerminalViewport({
417418
418419 const readSelectionAction = (): {
419420 position: { x: number; y: number };
421 clipboardText: string;
420422 selection: TerminalContextSelection;
421423 } | null => {
422424 const activeTerminal = terminalRef.current;
......@@ -445,6 +447,7 @@ export function TerminalViewport({
445447 });
446448 return {
447449 position,
450 clipboardText: selectionText,
448451 selection: {
449452 terminalId,
450453 terminalLabel: readTerminalLabel(),
......@@ -460,7 +463,7 @@ export function TerminalViewport({
460463 clearSelectionAction();
461464 return;
462465 }
463 if (selectionActionOpenRef.current) {
466 if (selectionActionMenuOpenRef.current) {
464467 return;
465468 }
466469 const nextAction = readSelectionAction();
......@@ -469,20 +472,46 @@ export function TerminalViewport({
469472 return;
470473 }
471474 const requestId = ++selectionActionRequestIdRef.current;
472 selectionActionOpenRef.current = true;
473 try {
474 const clicked = await localApi.contextMenu.show(
475 [{ id: "add-to-chat", label: "Add to chat" }],
475 selectionActionMenuOpenRef.current = true;
476 const clicked = await localApi.contextMenu
477 .show(
478 [
479 { id: "add-to-chat", label: "Add to chat" },
480 { id: "copy", label: "Copy" },
481 ],
476482 nextAction.position,
477 );
478 if (requestId !== selectionActionRequestIdRef.current || clicked !== "add-to-chat") {
483 )
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 }
479514 return;
480 }
481 handleAddTerminalContext(nextAction.selection);
482 terminalRef.current?.clearSelection();
483 terminalRef.current?.focus();
484 } finally {
485 selectionActionOpenRef.current = false;
486515 }
487516 };
488517