mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] shortcuts: Cmd+C attaches the selection straight into composer context, Cmd+Z undoes card closes
This commit is contained in:
@@ -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) ----
|
||||
|
||||
Reference in New Issue
Block a user