[eric] shortcuts: Cmd+C attaches the selection straight into composer context, Cmd+Z undoes card closes

This commit is contained in:
ciregenz
2026-08-06 10:58:31 -07:00
parent a64fcef234
commit 0ba7ca6d19
5 changed files with 60 additions and 30 deletions
@@ -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<string, SelectedElement['semanticType']> = {
agent: 'agent-card',
view: 'view-card',
browser: 'browser-card',
};
const semanticType = semanticTypeMap[card.type];
if (!semanticType) return null;
const labelMap: Record<string, string> = {
'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<typeof useElementSelection>, ownerId: string): boolean {
const copied = getClipboardCards();
if (copied.length === 0 || !elementSelection) return false;
for (const card of copied) {
const semanticTypeMap: Record<string, SelectedElement['semanticType']> = {
agent: 'agent-card',
view: 'view-card',
browser: 'browser-card',
};
const semanticType = semanticTypeMap[card.type];
if (!semanticType) continue;
const labelMap: Record<string, string> = {
'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;
@@ -54,7 +54,7 @@ interface Props {
prefillMode?: string;
}
const TOOLBAR_OWNER_ID = '__toolbar__';
export const TOOLBAR_OWNER_ID = '__toolbar__';
const MotionBox = motion.div;
@@ -23,6 +23,7 @@ interface UseDashboardClipboardArgs {
browserCards: Record<string, BrowserCardPosition>;
outputs: Record<string, Output>;
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) => {
@@ -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());
};
@@ -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) ----