From 93c64589bc944cf677c374e539e4cd4d9f9ccee1 Mon Sep 17 00:00:00 2001 From: "Jose R. Perez" Date: Mon, 16 Mar 2026 13:39:49 -0400 Subject: [PATCH 1/2] feat: add copy prompt button to task card header Adds a hover-revealed copy-to-clipboard icon button in the card header row of BoardCard. Clicking it copies the full raw card.prompt to the clipboard and briefly shows a 'Copied!' tooltip for visual feedback. --- web-ui/src/components/board-card.tsx | 69 +++++++++++++++++++--------- 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/web-ui/src/components/board-card.tsx b/web-ui/src/components/board-card.tsx index ecbc2d2..b0829bf 100644 --- a/web-ui/src/components/board-card.tsx +++ b/web-ui/src/components/board-card.tsx @@ -1,21 +1,25 @@ import { Draggable } from "@hello-pangea/dnd"; -import { GitBranch, Play, RotateCcw, Trash2 } from "lucide-react"; +import { buildTaskWorktreeDisplayPath } from "@runtime-task-worktree-path"; +import { Copy, GitBranch, Play, RotateCcw, Trash2 } from "lucide-react"; import type { MouseEvent } from "react"; import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; -import { buildTaskWorktreeDisplayPath } from "@runtime-task-worktree-path"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/components/ui/cn"; +import { Spinner } from "@/components/ui/spinner"; +import { Tooltip } from "@/components/ui/tooltip"; import type { RuntimeTaskSessionSummary } from "@/runtime/types"; import { useTaskWorkspaceSnapshotValue } from "@/stores/workspace-metadata-store"; import type { BoardCard as BoardCardModel, BoardColumnId } from "@/types"; import { getTaskAutoReviewCancelButtonLabel } from "@/types"; import { formatPathForDisplay } from "@/utils/path-display"; import { useMeasure } from "@/utils/react-use"; -import { clampTextWithInlineSuffix, splitPromptToTitleDescriptionByWidth, truncateTaskPromptLabel } from "@/utils/task-prompt"; +import { + clampTextWithInlineSuffix, + splitPromptToTitleDescriptionByWidth, + truncateTaskPromptLabel, +} from "@/utils/task-prompt"; import { DEFAULT_TEXT_MEASURE_FONT, measureTextWidth, readElementFontShorthand } from "@/utils/text-measure"; -import { Button } from "@/components/ui/button"; -import { Spinner } from "@/components/ui/spinner"; -import { Tooltip } from "@/components/ui/tooltip"; -import { cn } from "@/components/ui/cn"; interface CardSessionActivity { dotColor: string; @@ -152,6 +156,7 @@ export function BoardCard({ const [titleFont, setTitleFont] = useState(DEFAULT_TEXT_MEASURE_FONT); const [descriptionFont, setDescriptionFont] = useState(DEFAULT_TEXT_MEASURE_FONT); const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false); + const [isCopied, setIsCopied] = useState(false); const reviewWorkspaceSnapshot = useTaskWorkspaceSnapshotValue(card.id); const isTrashCard = columnId === "trash"; const isCardInteractive = !isTrashCard; @@ -273,6 +278,14 @@ export function BoardCard({ const cancelAutomaticActionLabel = !isTrashCard && card.autoReviewEnabled ? getTaskAutoReviewCancelButtonLabel(card.autoReviewMode) : null; + const handleCopyPrompt = (event: MouseEvent) => { + stopEvent(event); + navigator.clipboard.writeText(card.prompt).then(() => { + setIsCopied(true); + setTimeout(() => setIsCopied(false), 1500); + }); + }; + return ( {(provided, snapshot) => { @@ -350,9 +363,7 @@ export function BoardCard({ )} >
- {statusMarker ? ( -
{statusMarker}
- ) : null} + {statusMarker ?
{statusMarker}
: null}

+ {isHovered && ( + +
{displayPromptSplit.description ? (