[eric] share: chats copy their answer and export as .swarm from the card menu and dock tiles

This commit is contained in:
ciregenz
2026-08-03 20:51:00 -07:00
parent 331478d9fd
commit f4d92cc0bd
6 changed files with 55 additions and 1 deletions
@@ -26,6 +26,7 @@ import { ackRun, runWorkflowNow } from '@/shared/state/workflowsSlice';
import { setPendingBrowserUrl } from '@/shared/state/tempStateSlice';
import { fetchOutputs } from '@/shared/state/outputsSlice';
import UpdateReadyPill from '@/app/components/Layout/UpdateReadyPill';
import ShareRequestHost from '@/app/components/share/ShareRequestHost';
import { findBrowserByWebContentsId } from '@/shared/browserRegistry';
import { byPreviewRecency } from '@/shared/previewOrder';
import { useClaudeTokens, useThemeAccent, useThemeWash } from '@/shared/styles/ThemeContext';
@@ -574,6 +575,8 @@ const AppShell: React.FC = () => {
<Settings />
</React.Suspense>
<ShareRequestHost />
</Box>
);
};
@@ -0,0 +1,27 @@
import React, { useEffect, useState } from 'react';
import ShareModal from './ShareModal';
import type { ShareTarget } from './shareTypes';
export const SHARE_REQUEST_EVENT = 'openswarm:share-entity';
export function requestShare(target: ShareTarget): void {
window.dispatchEvent(new CustomEvent(SHARE_REQUEST_EVENT, { detail: target }));
}
/** One global mount that turns share requests from stateless menu rows (card context menu, dock
* tiles) into the ShareModal; the row can't own modal state because the menu unmounts on click. */
const ShareRequestHost: React.FC = () => {
const [target, setTarget] = useState<ShareTarget | null>(null);
useEffect(() => {
const onShare = (e: Event): void => {
const detail = (e as CustomEvent).detail as ShareTarget | undefined;
if (detail && detail.kind && detail.id) setTarget(detail);
};
window.addEventListener(SHARE_REQUEST_EVENT, onShare);
return () => window.removeEventListener(SHARE_REQUEST_EVENT, onShare);
}, []);
if (!target) return null;
return <ShareModal target={target} open onClose={() => setTarget(null)} />;
};
export default ShareRequestHost;
@@ -1,6 +1,6 @@
// Shared types for the .swarm share/import UI. The *Response shapes mirror the backend pydantic models in backend/apps/swarm/models.py; keep them in sync.
export type ShareKind = 'skill' | 'app' | 'workflow' | 'dashboard';
export type ShareKind = 'skill' | 'app' | 'workflow' | 'dashboard' | 'session';
export interface ShareTarget {
kind: ShareKind;
@@ -1,6 +1,9 @@
import { clearSessionMessages, deleteSession, duplicateSession, expandSession, collapseSession, stopAgent, fetchSession, type AgentSession } from '@/shared/state/agentsSlice';
import { placeCard, bringToFront } from '@/shared/state/dashboardLayoutSlice';
import { setClipboardCards } from '@/shared/dashboardClipboard';
import { copySessionResponse } from '@/shared/copySessionResponse';
import { requestShare } from '@/app/components/share/ShareRequestHost';
import { displaySessionName } from '@/shared/state/sessionDisplay';
import { handleSlashCommand } from '@/app/pages/AgentChat/ChatInput/hooks/slashCommands';
import type { AppDispatch } from '@/shared/state/store';
import type { CardMenuRow } from '../desktop/openCardContextMenu';
@@ -46,6 +49,8 @@ export function agentCardMenuRows({ session, dispatch, expanded, tileZone, expan
x: card.x, y: card.y, width: card.width, height: card.height, expanded,
}]),
},
{ label: 'Copy response', onClick: () => { copySessionResponse(session.id); } },
{ label: 'Share as .swarm…', onClick: () => requestShare({ kind: 'session', id: session.id, name: displaySessionName(session.name) }) },
{ kind: 'separator' },
{ kind: 'header', label: 'Session' },
{ label: 'Stop turn', disabled: !running, onClick: () => { void dispatch(stopAgent({ sessionId: session.id })); } },
@@ -1,5 +1,7 @@
import { bringToFront, recordClosedCard, removeCard, removeWorkflowCard } from '@/shared/state/dashboardLayoutSlice';
import { closeSession } from '@/shared/state/agentsSlice';
import { copySessionResponse } from '@/shared/copySessionResponse';
import { requestShare } from '@/app/components/share/ShareRequestHost';
import { removeBrowserCardCleanly } from '@/shared/browserTeardown';
import { removeViewCardCleanly } from '@/shared/viewTeardown';
import type { AppDispatch } from '@/shared/state/store';
@@ -10,6 +12,10 @@ export function dockTileMenuRows(entry: DockEntry, dispatch: AppDispatch, onFocu
return [
{ label: 'Show on canvas', onClick: onFocus },
{ label: 'Bring to front', onClick: () => dispatch(bringToFront({ id: entry.id, type: entry.kind })) },
...(entry.kind === 'agent' ? [
{ label: 'Copy response', onClick: () => { copySessionResponse(entry.id); } } as CardMenuRow,
{ label: 'Share as .swarm…', onClick: () => requestShare({ kind: 'session', id: entry.id, name: entry.label }) } as CardMenuRow,
] : []),
{ kind: 'separator' },
{
label: 'Close',
@@ -0,0 +1,13 @@
import { store } from '@/shared/state/store';
/** Copy the chat's latest assistant answer to the clipboard; true when something was copied.
* Menu rows (card context menu, dock tiles) call this statelessly. */
export function copySessionResponse(sessionId: string): boolean {
const session = store.getState().agents.sessions[sessionId];
const last = [...(session?.messages ?? [])]
.reverse()
.find((m) => m.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
if (!last) return false;
void navigator.clipboard.writeText(last.content as string);
return true;
}