diff --git a/frontend/src/app/pages/AgentChat/ChatInput/hooks/pasteCards.ts b/frontend/src/app/pages/AgentChat/ChatInput/hooks/pasteCards.ts index 7f66f531..d2462836 100644 --- a/frontend/src/app/pages/AgentChat/ChatInput/hooks/pasteCards.ts +++ b/frontend/src/app/pages/AgentChat/ChatInput/hooks/pasteCards.ts @@ -1,37 +1,42 @@ import { useElementSelection, SelectedElement } from '@/app/components/editor/ElementSelectionContext'; -import { getClipboardCards, clearClipboard } from '@/shared/dashboardClipboard'; +import { getClipboardCards, clearClipboard, type ClipboardCard } from '@/shared/dashboardClipboard'; + +/** One clipboard card -> the same SelectedElement shape select mode produces; null for kinds context can't hold. */ +export function clipboardCardToSelectedElement(card: ClipboardCard): SelectedElement | null { + const semanticTypeMap: Record = { + agent: 'agent-card', + view: 'view-card', + browser: 'browser-card', + }; + const semanticType = semanticTypeMap[card.type]; + if (!semanticType) return null; + const labelMap: Record = { + 'agent-card': 'Agent', + 'view-card': 'View', + 'browser-card': 'Browser', + }; + const semanticLabel = (labelMap[semanticType] || semanticType) + ': ' + card.name; + return { + id: `sel-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + selectorPath: `[data-select-type="${semanticType}"][data-select-id="${card.id}"]`, + tagName: 'DIV', + className: '', + outerHTML: '', + computedStyles: {}, + boundingRect: { x: 0, y: 0, width: 0, height: 0 }, + semanticType, + semanticLabel, + semanticData: { ...card.meta, selectId: card.id }, + }; +} /** Drains the dashboard clipboard into owner-scoped selected elements. Returns true if it consumed the paste. */ export function tryPasteClipboardCards(elementSelection: ReturnType, ownerId: string): boolean { const copied = getClipboardCards(); if (copied.length === 0 || !elementSelection) return false; for (const card of copied) { - const semanticTypeMap: Record = { - agent: 'agent-card', - view: 'view-card', - browser: 'browser-card', - }; - const semanticType = semanticTypeMap[card.type]; - if (!semanticType) continue; - const labelMap: Record = { - 'agent-card': 'Agent', - 'view-card': 'View', - 'browser-card': 'Browser', - }; - const semanticLabel = (labelMap[semanticType] || semanticType) + ': ' + card.name; - const el: SelectedElement = { - id: `sel-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, - selectorPath: `[data-select-type="${semanticType}"][data-select-id="${card.id}"]`, - tagName: 'DIV', - className: '', - outerHTML: '', - computedStyles: {}, - boundingRect: { x: 0, y: 0, width: 0, height: 0 }, - semanticType, - semanticLabel, - semanticData: { ...card.meta, selectId: card.id }, - }; - elementSelection.addElementForOwner(ownerId, el); + const el = clipboardCardToSelectedElement(card); + if (el) elementSelection.addElementForOwner(ownerId, el); } clearClipboard(); return true; diff --git a/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx b/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx index 61b45fee..cfa57901 100644 --- a/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx +++ b/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx @@ -54,7 +54,7 @@ interface Props { prefillMode?: string; } -const TOOLBAR_OWNER_ID = '__toolbar__'; +export const TOOLBAR_OWNER_ID = '__toolbar__'; const MotionBox = motion.div; diff --git a/frontend/src/app/pages/Dashboard/hooks/interaction/useDashboardClipboard.ts b/frontend/src/app/pages/Dashboard/hooks/interaction/useDashboardClipboard.ts index 984bd89c..d4697a8b 100644 --- a/frontend/src/app/pages/Dashboard/hooks/interaction/useDashboardClipboard.ts +++ b/frontend/src/app/pages/Dashboard/hooks/interaction/useDashboardClipboard.ts @@ -23,6 +23,7 @@ interface UseDashboardClipboardArgs { browserCards: Record; outputs: Record; expandedSessionIds: string[]; + onCopiedToContext?: (copied: ClipboardCard[]) => void; } export function useDashboardClipboard({ @@ -35,6 +36,7 @@ export function useDashboardClipboard({ browserCards, outputs, expandedSessionIds, + onCopiedToContext, }: UseDashboardClipboardArgs) { const dispatch = useAppDispatch(); @@ -89,10 +91,12 @@ export function useDashboardClipboard({ } setClipboardCards(copied); navigator.clipboard.writeText(names.join(', ')).catch(() => {}); + // Copy IS attach: the selection lands in the composer as context chips, no select mode, no paste step. + if (copied.length > 0) onCopiedToContext?.(copied); }; window.addEventListener('keydown', handleCopy); return () => window.removeEventListener('keydown', handleCopy); - }, [selection.selectedIds, sessions, cards, viewCards, browserCards, outputs, expandedSessionIds]); + }, [selection.selectedIds, sessions, cards, viewCards, browserCards, outputs, expandedSessionIds, onCopiedToContext]); useEffect(() => { const handlePaste = (e: KeyboardEvent) => { diff --git a/frontend/src/app/pages/Dashboard/hooks/interaction/useDashboardShortcuts.ts b/frontend/src/app/pages/Dashboard/hooks/interaction/useDashboardShortcuts.ts index 9e469652..f9de1775 100644 --- a/frontend/src/app/pages/Dashboard/hooks/interaction/useDashboardShortcuts.ts +++ b/frontend/src/app/pages/Dashboard/hooks/interaction/useDashboardShortcuts.ts @@ -93,7 +93,15 @@ export function useDashboardShortcuts({ useEffect(() => { const handleReopen = (e: KeyboardEvent) => { if (!isActive) return; - if (!(e.metaKey || e.ctrlKey) || !e.shiftKey || e.altKey || e.key.toLowerCase() !== 't') return; + if (!(e.metaKey || e.ctrlKey) || e.altKey) return; + const isReopenT = e.shiftKey && e.key.toLowerCase() === 't'; + // Cmd+Z = undo my last close (chats, browsers, apps), the muscle memory for "I deleted that by accident"; editables keep text-undo. + const isUndoZ = !e.shiftKey && e.key.toLowerCase() === 'z'; + if (!isReopenT && !isUndoZ) return; + if (isUndoZ) { + const t = e.target as HTMLElement; + if (t?.tagName === 'INPUT' || t?.tagName === 'TEXTAREA' || t?.isContentEditable) return; + } e.preventDefault(); dispatch(reopenLastClosed()); }; diff --git a/frontend/src/app/pages/Dashboard/hooks/state/useDashboardController.ts b/frontend/src/app/pages/Dashboard/hooks/state/useDashboardController.ts index e6384046..f7bfdf6d 100644 --- a/frontend/src/app/pages/Dashboard/hooks/state/useDashboardController.ts +++ b/frontend/src/app/pages/Dashboard/hooks/state/useDashboardController.ts @@ -2,6 +2,9 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useAppSelector } from '@/shared/hooks'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; import { useElementSelection } from '@/app/components/editor/ElementSelectionContext'; +import { clipboardCardToSelectedElement } from '@/app/pages/AgentChat/ChatInput/hooks/pasteCards'; +import { TOOLBAR_OWNER_ID } from '@/app/pages/Dashboard/DashboardToolbar'; +import type { ClipboardCard } from '@/shared/dashboardClipboard'; import { useCanvasControls } from '../interaction/useCanvasControls'; import { useDashboardSelection } from './useDashboardSelection'; import { useDashboardSelectors } from './useDashboardSelectors'; @@ -236,6 +239,15 @@ export function useDashboardController(dashboardId: string, isActive: boolean) { return () => window.removeEventListener('openswarm:dictation-fallback', onDictation); }, [isActive, handleStarter]); + const onCopiedToContext = useCallback((copied: ClipboardCard[]) => { + if (!elementSelectionCtx) return; + for (const card of copied) { + const el = clipboardCardToSelectedElement(card); + if (el) elementSelectionCtx.addElementForOwner(TOOLBAR_OWNER_ID, el); + } + setToolbarOpen(true); + }, [elementSelectionCtx, setToolbarOpen]); + useDashboardClipboard({ isActive, dashboardId, @@ -246,6 +258,7 @@ export function useDashboardController(dashboardId: string, isActive: boolean) { browserCards, outputs, expandedSessionIds, + onCopiedToContext, }); // ---- Arrow key card navigation (when zoomed in on a card) ----