From b8ca2535b9bda4dcc419f88e8a980a013ad6739d Mon Sep 17 00:00:00 2001 From: haikdc Date: Sat, 18 Apr 2026 13:23:57 -0700 Subject: [PATCH] [Haik]: Remove legacy ChatInput component tree (ChatInput, AttachmentChips, ImageAttachments, useChatSubmit) and replace DashboardToolbar usage with OpenSwarmComposer via a new ToolbarComposer wrapper backed by useStandaloneComposerRuntime and AssistantRuntimeProvider; relocate ModelModeSelector and ContextRing into OpenSwarmComposer/components; move TabLocalState from shared Dashboard types to BrowserCard directory; add embedded prop to OpenSwarmComposer for borderless toolbar rendering --- .../pages/AgentChat/ChatInput/ChatInput.tsx | 170 ------------ .../ChatInput/components/AttachmentChips.tsx | 118 --------- .../ChatInput/components/ImageAttachments.tsx | 85 ------ .../ChatInput/components/useChatSubmit.ts | 241 ------------------ .../OpenSwarmComposer/OpenSwarmComposer.tsx | 12 +- .../ModelModeSelector/ContextRing.tsx | 0 .../ModelModeSelector/ModelModeSelector.tsx | 0 .../runtime/useStandaloneComposerRuntime.ts | 39 +++ .../cards/BrowserCard/BrowserCard.tsx | 2 +- .../cards/BrowserCard/TabLocalState.ts | 6 + .../BrowserCard/components/BrowserTabBar.tsx | 2 +- .../BrowserCard/hooks/useWebviewLifecycle.ts | 2 +- .../DashboardToolbar/DashboardToolbar.tsx | 52 +++- .../src/app/pages/Dashboard/types/types.ts | 6 - 14 files changed, 101 insertions(+), 634 deletions(-) delete mode 100644 frontend/src/app/pages/AgentChat/ChatInput/ChatInput.tsx delete mode 100644 frontend/src/app/pages/AgentChat/ChatInput/components/AttachmentChips.tsx delete mode 100644 frontend/src/app/pages/AgentChat/ChatInput/components/ImageAttachments.tsx delete mode 100644 frontend/src/app/pages/AgentChat/ChatInput/components/useChatSubmit.ts rename frontend/src/app/pages/AgentChat/{ => OpenSwarmComposer/components}/ModelModeSelector/ContextRing.tsx (100%) rename frontend/src/app/pages/AgentChat/{ => OpenSwarmComposer/components}/ModelModeSelector/ModelModeSelector.tsx (100%) create mode 100644 frontend/src/app/pages/AgentChat/runtime/useStandaloneComposerRuntime.ts create mode 100644 frontend/src/app/pages/Dashboard/DashboardCanvas/cards/BrowserCard/TabLocalState.ts diff --git a/frontend/src/app/pages/AgentChat/ChatInput/ChatInput.tsx b/frontend/src/app/pages/AgentChat/ChatInput/ChatInput.tsx deleted file mode 100644 index 78469f96..00000000 --- a/frontend/src/app/pages/AgentChat/ChatInput/ChatInput.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import React, { useState, useRef, useEffect, useId, forwardRef, useImperativeHandle } from 'react'; -import Box from '@mui/material/Box'; -import Typography from '@mui/material/Typography'; -import AttachFileIcon from '@mui/icons-material/AttachFile'; -import CommandPicker from '@/app/components/CommandPicker'; -import { useElementSelection } from '@/app/components/ElementSelectionContext'; -import type { ContextPath } from '@/shared/state/agentsTypes'; -import { type AttachedSkill, type TriggerState, EMPTY_TRIGGER, serializeEditorContent } from '@/app/components/richEditorUtils'; -import { useAppSelector } from '@/shared/hooks'; -import { useClaudeTokens } from '@/shared/styles/ThemeContext'; -import type { AttachedImage } from './components/ImageAttachments'; -import ImageAttachments from './components/ImageAttachments'; -import type { ForcedToolGroup } from './components/AttachmentChips'; -import AttachmentChips from './components/AttachmentChips'; -import ModelModeSelector from '@/app/pages/AgentChat/ModelModeSelector/ModelModeSelector'; -import { useChatSubmit } from './components/useChatSubmit'; - -interface Props { - onSend: (message: string, images?: Array<{ data: string; media_type: string }>, contextPaths?: ContextPath[], forcedTools?: string[], attachedSkills?: Array<{ id: string; name: string; content: string }>, selectedBrowserIds?: string[]) => void; - disabled?: boolean; - mode: string; onModeChange: (mode: string) => void; - model: string; onModelChange: (model: string) => void; - provider?: string; onProviderChange?: (provider: string) => void; - isRunning?: boolean; onStop?: () => void; - autoRunMode?: boolean; - contextEstimate?: { used: number; limit: number }; - embedded?: boolean; autoFocus?: boolean; - sessionId?: string; queueLength?: number; -} - -export interface ChatInputHandle { - getConfig: () => { prompt: string; contextPaths: ContextPath[]; forcedTools: ForcedToolGroup[] }; - setContent: (prompt: string, contextPaths?: ContextPath[], forcedTools?: ForcedToolGroup[]) => void; -} - -const ChatInput = forwardRef(({ - onSend, disabled, mode, onModeChange, model, onModelChange, provider, onProviderChange, - isRunning, onStop, autoRunMode, contextEstimate, embedded, autoFocus, sessionId, queueLength = 0, -}, ref) => { - const c = useClaudeTokens(); - const editorRef = useRef(null); - const containerRef = useRef(null); - const elementSelection = useElementSelection(); - - const fallbackOwnerId = useId(); - const ownerId = sessionId || fallbackOwnerId; - - useEffect(() => { if (autoFocus) editorRef.current?.focus(); }, [autoFocus]); - - const [hasContent, setHasContent] = useState(false); - const [attachedSkills, setAttachedSkills] = useState>({}); - const attachedSkillsRef = useRef(attachedSkills); - useEffect(() => { attachedSkillsRef.current = attachedSkills; }); - const [picker, setPicker] = useState(EMPTY_TRIGGER); - const skills = useAppSelector((state) => state.skills.items); - const modesMap = useAppSelector((state) => state.modes.items); - - const [images, setImages] = useState([]); - const [lightboxSrc, setLightboxSrc] = useState(null); - const [isDragOver, setIsDragOver] = useState(false); - const [contextPaths, setContextPaths] = useState([]); - const [forcedTools, setForcedTools] = useState([]); - const [copiedPathIdx, setCopiedPathIdx] = useState(null); - - useImperativeHandle(ref, () => ({ - getConfig: () => { - const editor = editorRef.current; - const prompt = editor ? serializeEditorContent(editor, attachedSkillsRef.current).trim() : ''; - return { prompt, contextPaths, forcedTools }; - }, - setContent: (prompt: string, newContextPaths?: ContextPath[], newForcedTools?: ForcedToolGroup[]) => { - const editor = editorRef.current; - if (editor) { editor.textContent = prompt; setHasContent(!!prompt); } - if (newContextPaths) setContextPaths(newContextPaths); - if (newForcedTools) setForcedTools(newForcedTools); - }, - }), [contextPaths, forcedTools]); - - const { - handleSend, handlePickerSelect, handlePaste, handleKeyDown, - handleInput, handleEditorClick, handleDragOver, handleDragLeave, handleDrop, - addImageFiles, browseAndAttachFiles, removeImage, - } = useChatSubmit({ - editorRef, attachedSkillsRef, disabled, autoRunMode, - images, contextPaths, forcedTools, picker, skills, ownerId, - elementSelection, onSend, onModeChange, setImages, setContextPaths, - setForcedTools, setPicker, setHasContent, setAttachedSkills, - setIsDragOver, c, - }); - - const handleCopyPath = (idx: number) => { - navigator.clipboard.writeText(contextPaths[idx].path); - setCopiedPathIdx(idx); - setTimeout(() => setCopiedPathIdx((cur) => cur === idx ? null : cur), 1200); - }; - - const selectedElements = elementSelection?.elementsByOwner?.[ownerId] ?? []; - const hasAttachments = images.length > 0 || contextPaths.length > 0 || forcedTools.length > 0 || selectedElements.length > 0; - const modeLabel = modesMap[mode]?.name || 'Agent'; - - return ( - - - {isDragOver && ( - - - Drop files here - - )} - - setPicker((prev) => ({ ...prev, visible: false }))} - visible={picker.visible} /> - - setLightboxSrc(null)} c={c} /> - - setContextPaths((prev) => prev.filter((_, i) => i !== idx))} - copiedPathIdx={copiedPathIdx} onCopyPath={handleCopyPath} - forcedTools={forcedTools} - onRemoveForcedTool={(idx) => setForcedTools((prev) => prev.filter((_, i) => i !== idx))} - selectedElements={selectedElements} - onRemoveElement={(id) => elementSelection?.removeOwnerElement(ownerId, id)} - hasImages={images.length > 0} c={c} /> - - -
- {!hasContent && ( -
- {disabled ? 'Agent is working...' : autoRunMode ? 'Describe what data to generate…' : isRunning ? (queueLength > 0 ? `${queueLength} queued — type another or wait…` : 'Agent is working — messages will queue…') : `${modeLabel}, @ for context, / for commands`} -
- )} - - - - - ); -}); - -ChatInput.displayName = 'ChatInput'; - -export default ChatInput; diff --git a/frontend/src/app/pages/AgentChat/ChatInput/components/AttachmentChips.tsx b/frontend/src/app/pages/AgentChat/ChatInput/components/AttachmentChips.tsx deleted file mode 100644 index cc7c3d87..00000000 --- a/frontend/src/app/pages/AgentChat/ChatInput/components/AttachmentChips.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import React from 'react'; -import Box from '@mui/material/Box'; -import Chip from '@mui/material/Chip'; -import Tooltip from '@mui/material/Tooltip'; -import FolderOpenIcon from '@mui/icons-material/FolderOpen'; -import InsertDriveFileOutlinedIcon from '@mui/icons-material/InsertDriveFileOutlined'; -import AdsClickIcon from '@mui/icons-material/AdsClick'; -import { getToolGroupIcon } from '@/app/components/CommandPicker'; -import type { SelectedElement } from '@/app/components/ElementSelectionContext'; -import type { ContextPath } from '@/shared/state/agentsTypes'; - -export interface ForcedToolGroup { - label: string; - tools: string[]; - icon?: React.ReactNode; - iconKey?: string; -} - -interface Props { - contextPaths: ContextPath[]; - onRemoveContextPath: (idx: number) => void; - copiedPathIdx: number | null; - onCopyPath: (idx: number) => void; - forcedTools: ForcedToolGroup[]; - onRemoveForcedTool: (idx: number) => void; - selectedElements: SelectedElement[]; - onRemoveElement: (id: string) => void; - hasImages: boolean; - c: { - accent: { primary: string }; - font: { mono: string }; - status: { error: string; info: string }; - }; -} - -const AttachmentChips: React.FC = ({ - contextPaths, onRemoveContextPath, copiedPathIdx, onCopyPath, - forcedTools, onRemoveForcedTool, - selectedElements, onRemoveElement, - hasImages, c, -}) => ( - <> - {contextPaths.length > 0 && ( - - {contextPaths.map((cp, idx) => { - const label = cp.path.split('/').filter(Boolean).slice(-2).join('/'); - return ( - - : } - label={label} size="small" - onClick={() => onCopyPath(idx)} - onDelete={() => onRemoveContextPath(idx)} - sx={{ - bgcolor: `${c.accent.primary}12`, color: c.accent.primary, - fontSize: '0.72rem', fontFamily: c.font.mono, height: 26, maxWidth: 220, cursor: 'pointer', - '& .MuiChip-label': { overflow: 'hidden', textOverflow: 'ellipsis' }, - '& .MuiChip-deleteIcon': { color: c.accent.primary, fontSize: 16, '&:hover': { color: c.status.error } }, - }} - /> - - ); - })} - - )} - - {forcedTools.length > 0 && ( - 0) ? 0.25 : 1, pb: 0 }}> - {forcedTools.map((ft, idx) => ( - {ft.icon || getToolGroupIcon(ft.iconKey || ft.label, 14)}} - label={`@${ft.label.toLowerCase()}`} size="small" - onDelete={() => onRemoveForcedTool(idx)} - sx={{ - bgcolor: `${c.status.info}15`, color: c.status.info, - fontSize: '0.72rem', fontFamily: c.font.mono, height: 26, maxWidth: 220, - '& .MuiChip-label': { overflow: 'hidden', textOverflow: 'ellipsis' }, - '& .MuiChip-deleteIcon': { color: c.status.info, fontSize: 16, '&:hover': { color: c.status.error } }, - }} - /> - ))} - - )} - - {selectedElements.length > 0 && ( - 0 || forcedTools.length > 0) ? 0.25 : 1, pb: 0 }}> - {selectedElements.map((el) => { - const chipLabel = el.semanticLabel ? el.semanticLabel - : el.className ? `${el.tagName.toLowerCase()}.${el.className.split(' ')[0]}` - : el.tagName.toLowerCase(); - const tooltipText = el.semanticType - ? `${el.semanticType}: ${el.semanticLabel || el.selectorPath}` - : el.selectorPath; - return ( - - } label={chipLabel} size="small" - onDelete={() => onRemoveElement(el.id)} - sx={{ - bgcolor: 'rgba(59, 130, 246, 0.1)', color: '#3b82f6', - fontSize: '0.72rem', fontFamily: c.font.mono, height: 26, maxWidth: 220, - '& .MuiChip-label': { overflow: 'hidden', textOverflow: 'ellipsis' }, - '& .MuiChip-deleteIcon': { color: '#3b82f6', fontSize: 16, '&:hover': { color: c.status.error } }, - '& .MuiChip-icon': { color: '#3b82f6' }, - }} - /> - - ); - })} - - )} - -); - -export default AttachmentChips; diff --git a/frontend/src/app/pages/AgentChat/ChatInput/components/ImageAttachments.tsx b/frontend/src/app/pages/AgentChat/ChatInput/components/ImageAttachments.tsx deleted file mode 100644 index c058b0c8..00000000 --- a/frontend/src/app/pages/AgentChat/ChatInput/components/ImageAttachments.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import React from 'react'; -import Box from '@mui/material/Box'; -import IconButton from '@mui/material/IconButton'; -import Modal from '@mui/material/Modal'; -import CloseIcon from '@mui/icons-material/Close'; - -export interface AttachedImage { - data: string; - media_type: string; - preview: string; -} - -interface Props { - images: AttachedImage[]; - onRemoveImage: (idx: number) => void; - lightboxSrc: string | null; - onOpenLightbox: (src: string) => void; - onCloseLightbox: () => void; - c: { - border: { subtle: string; medium: string }; - bg: { surface: string; secondary: string }; - text: { secondary: string; tertiary: string; primary: string }; - shadow: { md: string }; - }; -} - -const ImageAttachments: React.FC = ({ - images, onRemoveImage, lightboxSrc, onOpenLightbox, onCloseLightbox, c, -}) => ( - <> - {images.length > 0 && ( - - {images.map((img, idx) => ( - onOpenLightbox(img.preview)}> - - { e.stopPropagation(); onRemoveImage(idx); }} sx={{ - position: 'absolute', top: -2, right: -2, width: 18, height: 18, - bgcolor: c.bg.surface, border: `1px solid ${c.border.medium}`, - color: c.text.tertiary, - '&:hover': { bgcolor: c.bg.secondary, color: c.text.primary }, - }}> - - - - ))} - - )} - - - - - - - e.stopPropagation()} - style={{ - maxWidth: '90vw', maxHeight: '90vh', borderRadius: 8, - boxShadow: '0 8px 32px rgba(0,0,0,0.4)', display: 'block', - }} - /> - - - -); - -export default ImageAttachments; diff --git a/frontend/src/app/pages/AgentChat/ChatInput/components/useChatSubmit.ts b/frontend/src/app/pages/AgentChat/ChatInput/components/useChatSubmit.ts deleted file mode 100644 index 78b2a82c..00000000 --- a/frontend/src/app/pages/AgentChat/ChatInput/components/useChatSubmit.ts +++ /dev/null @@ -1,241 +0,0 @@ -import React, { useCallback } from 'react'; -import type { CommandPickerItem } from '@/app/components/CommandPicker'; -import { useElementSelection, type SelectedElement } from '@/app/components/ElementSelectionContext'; -import { getClipboardCards, clearClipboard } from '@/shared/dashboardClipboard'; -import { getWebview } from '@/shared/browserRegistry'; -import type { ContextPath } from '@/shared/state/agentsTypes'; -import { - SKILL_PILL_ATTR, type AttachedSkill, createSkillPillElement, - serializeEditorContent, type TriggerState, detectEditorTrigger, -} from '@/app/components/richEditorUtils'; -import type { AttachedImage } from '../ImageAttachments'; -import type { ForcedToolGroup } from '../AttachmentChips'; - -interface ChatSubmitParams { - editorRef: React.RefObject; attachedSkillsRef: React.MutableRefObject>; - disabled?: boolean; autoRunMode?: boolean; images: AttachedImage[]; contextPaths: ContextPath[]; - forcedTools: ForcedToolGroup[]; picker: TriggerState; - skills: Record; ownerId: string; - elementSelection: ReturnType; - onSend: (msg: string, imgs?: Array<{ data: string; media_type: string }>, ctx?: ContextPath[], tools?: string[], skills?: Array<{ id: string; name: string; content: string }>, browserIds?: string[]) => void; - onModeChange: (mode: string) => void; - setImages: React.Dispatch>; setContextPaths: React.Dispatch>; - setForcedTools: React.Dispatch>; setPicker: React.Dispatch>; - setHasContent: React.Dispatch>; setAttachedSkills: React.Dispatch>>; - setIsDragOver: React.Dispatch>; - c: { font: { mono: string }; status: { error: string } }; -} - -export function useChatSubmit(p: ChatSubmitParams) { - const { - editorRef, attachedSkillsRef, disabled, autoRunMode, - images, contextPaths, forcedTools, picker, skills, ownerId, - elementSelection, onSend, onModeChange, setImages, setContextPaths, - setForcedTools, setPicker, setHasContent, setAttachedSkills, - setIsDragOver, c, - } = p; - const updateHasContent = useCallback(() => { - const editor = editorRef.current; - if (!editor) return; - const text = (editor.textContent || '').replace(/\u200B/g, ''); - setHasContent(text.trim().length > 0 || editor.querySelector(`[${SKILL_PILL_ATTR}]`) !== null); - }, []); - const syncAttachedSkills = useCallback(() => { - const editor = editorRef.current; - if (!editor) return; - const pillIds = new Set( - Array.from(editor.querySelectorAll(`[${SKILL_PILL_ATTR}]`)) - .map((el) => el.getAttribute(SKILL_PILL_ATTR)).filter(Boolean) as string[], - ); - setAttachedSkills((prev) => { - const prevKeys = Object.keys(prev); - if (prevKeys.length === pillIds.size && prevKeys.every((k) => pillIds.has(k))) return prev; - const next: Record = {}; - for (const [id, skill] of Object.entries(prev)) { if (pillIds.has(id)) next[id] = skill; } - return next; - }); - }, []); - const removeSkillPill = useCallback((skillId: string) => { - const editor = editorRef.current; - if (!editor) return; - const pill = editor.querySelector(`[${SKILL_PILL_ATTR}="${skillId}"]`); - if (pill) pill.remove(); - setAttachedSkills((prev) => { const { [skillId]: _, ...rest } = prev; return rest; }); - const text = (editor.textContent || '').replace(/\u200B/g, ''); - setHasContent(text.trim().length > 0 || editor.querySelector(`[${SKILL_PILL_ATTR}]`) !== null); - editor.focus(); - }, []); - const detectTrigger = useCallback(() => { const r = detectEditorTrigger(); r ? setPicker(r) : setPicker((prev) => ({ ...prev, visible: false })); }, []); - const handleInput = useCallback(() => { updateHasContent(); detectTrigger(); syncAttachedSkills(); }, [updateHasContent, detectTrigger, syncAttachedSkills]); - const handleEditorClick = useCallback(() => { detectTrigger(); }, [detectTrigger]); - const addImageFiles = useCallback((files: FileList | File[]) => { - Array.from(files).forEach((file) => { - if (!file.type.startsWith('image/')) return; - const reader = new FileReader(); - reader.onload = () => { - const result = reader.result as string; - setImages((prev) => [...prev, { data: result.split(',')[1], media_type: file.type, preview: result }]); - }; - reader.readAsDataURL(file); - }); - }, []); - const attachFiles = useCallback((files: File[]) => { - if (files.length === 0) return; - const newPaths: ContextPath[] = files - .map((f) => (f as File & { path?: string }).path) - .filter((p): p is string => Boolean(p)) - .map((p) => ({ path: p, type: 'file' as const })); - if (newPaths.length > 0) setContextPaths((prev) => [...prev, ...newPaths]); - }, []); - const browseAndAttachFiles = useCallback(async () => { - const result = await window.openswarm.showOpenDialog({ - properties: ['openFile', 'multiSelections'], - }); - if (result.canceled || !result.filePaths?.length) return; - const newPaths: ContextPath[] = result.filePaths.map((p) => ({ path: p, type: 'file' as const })); - setContextPaths((prev) => [...prev, ...newPaths]); - }, []); - const handleSend = useCallback(async () => { - const editor = editorRef.current; - if (!editor || disabled) return; - let trimmed = serializeEditorContent(editor, attachedSkillsRef.current).trim(); - if (!trimmed) return; - const selectedEls = elementSelection?.elementsByOwner?.[ownerId] ?? []; - let allImages = images.length > 0 ? images.map(({ data, media_type }) => ({ data, media_type })) : []; - if (selectedEls.length > 0) { - const lines: string[] = ['\n\n---\nSelected UI Elements:\n']; - for (let i = 0; i < selectedEls.length; i++) { - const el = selectedEls[i]; - if (el.semanticType === 'browser-card' && el.semanticData?.selectId) { - const wv = getWebview(el.semanticData.selectId as string); - const url = wv ? (el.semanticData.url || wv.getURL()) : (el.semanticData.url || ''); - const title = wv ? (el.semanticData.name || wv.getTitle()) : (el.semanticLabel || ''); - lines.push(`${i + 1}. [Browser Card] ${title}`, ` browser_id: ${el.semanticData.selectId}`); - if (url) lines.push(` URL: ${url}`); - lines.push(' (Use BrowserAgent with this browser_id to interact with it, or CreateBrowserAgent for a new browser)'); - } else if (el.semanticType && el.semanticData) { - const typeLabel = { 'agent-card': 'Agent Card', message: 'Message', 'tool-call': 'Tool Call', 'tool-group': 'Tool Group', 'view-card': 'App Card', 'browser-card': 'Browser Card', 'dom-element': 'Element' }[el.semanticType] || el.semanticType; - lines.push(`${i + 1}. [${typeLabel}] ${el.semanticLabel || ''}`); - const { selectId, ...rest } = el.semanticData; - if (selectId) lines.push(` ID: ${selectId}`); - const metaStr = Object.entries(rest).filter(([, v]) => v != null).map(([k, v]) => `${k}: ${typeof v === 'string' ? v : JSON.stringify(v)}`).join(', '); - if (metaStr) lines.push(` ${metaStr}`); - if (el.semanticType === 'agent-card' && selectId) lines.push(` (Use InvokeAgent with session_id "${selectId}" to query this agent with full conversation context)`); - } else { - const styleStr = Object.entries(el.computedStyles).map(([k, v]) => `${k}: ${v}`).join('; '); - lines.push(`${i + 1}. \`${el.selectorPath}\` (${el.tagName.toLowerCase()})`, ` Selector: ${el.selectorPath}`); - lines.push(` HTML: ${el.outerHTML.length > 500 ? el.outerHTML.slice(0, 500) + '...' : el.outerHTML}`); - if (styleStr) lines.push(` Key styles: ${styleStr}`); - } - lines.push(''); - if (el.screenshot) allImages.push({ data: el.screenshot.replace(/^data:image\/\w+;base64,/, ''), media_type: 'image/png' }); - } - trimmed += lines.join('\n'); - } - const allForcedToolNames = forcedTools.flatMap((ft) => ft.tools); - const currentSkills = Object.values(attachedSkillsRef.current); - const sendSkills = currentSkills.length > 0 ? currentSkills.map((s) => ({ id: s.id, name: s.name, content: s.content })) : undefined; - const browserIds = selectedEls.filter((el) => el.semanticType === 'browser-card' && el.semanticData?.selectId).map((el) => el.semanticData!.selectId as string); - onSend(trimmed, allImages.length > 0 ? allImages : undefined, contextPaths.length > 0 ? contextPaths : undefined, - allForcedToolNames.length > 0 ? allForcedToolNames : undefined, sendSkills, browserIds.length > 0 ? browserIds : undefined); - editor.innerHTML = ''; - setImages([]); setContextPaths([]); setForcedTools([]); setAttachedSkills({}); setHasContent(false); - elementSelection?.clearOwnerElements(ownerId); - }, [disabled, images, contextPaths, forcedTools, onSend, elementSelection, ownerId]); - const handlePickerSelect = (item: CommandPickerItem) => { - setPicker((prev) => ({ ...prev, visible: false })); - const editor = editorRef.current; - if (!editor) return; - editor.focus(); - const { triggerNode, triggerOffset, filter } = picker; - if (triggerNode && triggerNode.parentNode && editor.contains(triggerNode)) { - const endOffset = Math.min(triggerOffset + 1 + filter.length, triggerNode.length); - const range = document.createRange(); - range.setStart(triggerNode, triggerOffset); - range.setEnd(triggerNode, endOffset); - range.deleteContents(); - const sel = window.getSelection(); - if (sel) { sel.removeAllRanges(); sel.addRange(range); } - } - if (item.type === 'skill') { - const skill = skills[item.id]; - if (!skill || editor.querySelector(`[${SKILL_PILL_ATTR}="${skill.id}"]`)) return; - const pill = createSkillPillElement({ id: skill.id, name: skill.name, content: skill.content }, removeSkillPill, c.font.mono, c.status.error); - const sel = window.getSelection(); - if (sel && sel.rangeCount > 0) { - const range = sel.getRangeAt(0); - range.collapse(false); - range.insertNode(pill); - const spacer = document.createTextNode('\u200B'); - pill.after(spacer); - const newRange = document.createRange(); - newRange.setStartAfter(spacer); - newRange.collapse(true); - sel.removeAllRanges(); - sel.addRange(newRange); - } - setAttachedSkills((prev) => ({ ...prev, [skill.id]: { id: skill.id, name: skill.name, content: skill.content } })); - } else if (item.type === 'mode') { - onModeChange(item.id); - } else if (item.type === 'context') { - if (item.command === 'file') { browseAndAttachFiles(); return; } - else if (item.toolNames && item.toolNames.length > 0) setForcedTools((prev) => [...prev, { label: item.name, tools: item.toolNames!, icon: item.icon, iconKey: item.iconKey }]); - else document.execCommand('insertText', false, `@${item.command} `); - } - updateHasContent(); - setTimeout(() => editor.focus(), 0); - }; - const handleKeyDown = (e: React.KeyboardEvent) => { - if (picker.visible && ['ArrowDown', 'ArrowUp', 'Escape', 'Tab', 'Enter'].includes(e.key)) { e.preventDefault(); return; } - if ((e.ctrlKey || e.metaKey) && ['b', 'i', 'u'].includes(e.key.toLowerCase())) { e.preventDefault(); return; } - if (e.key === 'Enter' && !e.shiftKey && !autoRunMode) { e.preventDefault(); handleSend(); } - }; - const handlePaste = useCallback((e: React.ClipboardEvent) => { - const copied = getClipboardCards(); - if (copied.length > 0 && elementSelection) { - e.preventDefault(); - 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 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: (labelMap[semanticType] || semanticType) + ': ' + card.name, - semanticData: { ...card.meta, selectId: card.id }, - }; - elementSelection.addElementForOwner(ownerId, el); - } - clearClipboard(); - return; - } - const items = e.clipboardData?.items; - if (items) { - const imageFiles: File[] = []; - for (let i = 0; i < items.length; i++) { - if (items[i].type.startsWith('image/')) { const file = items[i].getAsFile(); if (file) imageFiles.push(file); } - } - if (imageFiles.length > 0) { e.preventDefault(); addImageFiles(imageFiles); return; } - } - e.preventDefault(); - const plain = e.clipboardData?.getData('text/plain'); - if (plain) document.execCommand('insertText', false, plain); - }, [addImageFiles, elementSelection, ownerId]); - const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.dataTransfer.types.includes('Files')) setIsDragOver(true); }, []); - const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragOver(false); }, []); - const handleDrop = useCallback((e: React.DragEvent) => { - e.preventDefault(); e.stopPropagation(); setIsDragOver(false); - if (e.dataTransfer.files.length === 0) return; - const allFiles = Array.from(e.dataTransfer.files); - const imageFiles = allFiles.filter((f) => f.type.startsWith('image/')); - const otherFiles = allFiles.filter((f) => !f.type.startsWith('image/')); - if (imageFiles.length > 0) addImageFiles(imageFiles); - if (otherFiles.length > 0) attachFiles(otherFiles); - }, [addImageFiles, attachFiles]); - const removeImage = useCallback((idx: number) => setImages((prev) => prev.filter((_, i) => i !== idx)), []); - - return { handleSend, handlePickerSelect, handlePaste, handleKeyDown, handleInput, handleEditorClick, handleDragOver, handleDragLeave, handleDrop, addImageFiles, attachFiles, browseAndAttachFiles, removeImage }; -} diff --git a/frontend/src/app/pages/AgentChat/OpenSwarmComposer/OpenSwarmComposer.tsx b/frontend/src/app/pages/AgentChat/OpenSwarmComposer/OpenSwarmComposer.tsx index 5c56fbb1..2c9c6f0a 100644 --- a/frontend/src/app/pages/AgentChat/OpenSwarmComposer/OpenSwarmComposer.tsx +++ b/frontend/src/app/pages/AgentChat/OpenSwarmComposer/OpenSwarmComposer.tsx @@ -8,7 +8,7 @@ import type { ContextPath } from '@/shared/state/agentsTypes'; import { useOpenSwarmMentionAdapter, type MentionItemMetadata } from './components/OpenSwarmMentionAdapter'; import { useComposerAttachments } from './components/useComposerAttachments'; import { MentionSelectOverride, MentionPopover, ComposerAttachmentChips } from './components/ComposerParts'; -import ModelModeSelector from '../ModelModeSelector/ModelModeSelector'; +import ModelModeSelector from './components/ModelModeSelector/ModelModeSelector'; interface OpenSwarmComposerProps { composerExtrasRef: MutableRefObject; @@ -23,12 +23,13 @@ interface OpenSwarmComposerProps { contextEstimate?: { used: number; limit: number }; autoFocus?: boolean; initialContextPaths?: ContextPath[]; + embedded?: boolean; } const OpenSwarmComposer: FC = ({ composerExtrasRef, mode, onModeChange, model, onModelChange, isRunning, onStop, sessionId, queueLength, contextEstimate, autoFocus, - initialContextPaths, + initialContextPaths, embedded, }) => { const aui = useAui(); const mentionAdapter = useOpenSwarmMentionAdapter(); @@ -115,12 +116,15 @@ const OpenSwarmComposer: FC = ({ att.forcedTools.length > 0 || Object.keys(att.attachedSkills).length > 0; return ( -
+
diff --git a/frontend/src/app/pages/AgentChat/ModelModeSelector/ContextRing.tsx b/frontend/src/app/pages/AgentChat/OpenSwarmComposer/components/ModelModeSelector/ContextRing.tsx similarity index 100% rename from frontend/src/app/pages/AgentChat/ModelModeSelector/ContextRing.tsx rename to frontend/src/app/pages/AgentChat/OpenSwarmComposer/components/ModelModeSelector/ContextRing.tsx diff --git a/frontend/src/app/pages/AgentChat/ModelModeSelector/ModelModeSelector.tsx b/frontend/src/app/pages/AgentChat/OpenSwarmComposer/components/ModelModeSelector/ModelModeSelector.tsx similarity index 100% rename from frontend/src/app/pages/AgentChat/ModelModeSelector/ModelModeSelector.tsx rename to frontend/src/app/pages/AgentChat/OpenSwarmComposer/components/ModelModeSelector/ModelModeSelector.tsx diff --git a/frontend/src/app/pages/AgentChat/runtime/useStandaloneComposerRuntime.ts b/frontend/src/app/pages/AgentChat/runtime/useStandaloneComposerRuntime.ts new file mode 100644 index 00000000..705c0e36 --- /dev/null +++ b/frontend/src/app/pages/AgentChat/runtime/useStandaloneComposerRuntime.ts @@ -0,0 +1,39 @@ +import { useCallback } from 'react'; +import { + useExternalStoreRuntime, + type ThreadMessageLike, + type AppendMessage, +} from '@assistant-ui/react'; +import type { MutableRefObject } from 'react'; +import type { ComposerExtras, DispatchableMessage } from './useOpenSwarmRuntime'; + +const EMPTY_MESSAGES: ThreadMessageLike[] = []; + +function extractText(message: AppendMessage): string { + for (const part of message.content) { + if (part.type === 'text') return part.text; + } + return ''; +} + +export function useStandaloneComposerRuntime( + composerExtrasRef: MutableRefObject, + dispatchMessage: (msg: DispatchableMessage) => void, +) { + const onNew = useCallback( + async (message: AppendMessage) => { + const text = extractText(message); + if (!text) return; + const extras = composerExtrasRef.current; + composerExtrasRef.current = {}; + dispatchMessage({ prompt: text, ...extras }); + }, + [composerExtrasRef, dispatchMessage], + ); + + return useExternalStoreRuntime({ + messages: EMPTY_MESSAGES, + isRunning: false, + onNew, + }); +} diff --git a/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/BrowserCard/BrowserCard.tsx b/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/BrowserCard/BrowserCard.tsx index e5aa07e4..c14552aa 100644 --- a/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/BrowserCard/BrowserCard.tsx +++ b/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/BrowserCard/BrowserCard.tsx @@ -11,7 +11,7 @@ import { useOverlayScrollPassthrough } from '../shared/useOverlayScrollPassthrou import { useElementSelection } from '@/app/components/ElementSelectionContext'; import { type ResizeDir, CURSOR_MAP, HANDLE_DEFS, DRAG_THRESHOLD } from '../shared/cardLayoutConstants'; import { useWebviewLifecycle, isElectron, chromeUserAgent, webviewPreloadPath, type WebviewElement } from './hooks/useWebviewLifecycle'; -import type { TabLocalState } from '@/app/pages/Dashboard/types/types'; +import type { TabLocalState } from './TabLocalState'; import BrowserTabBar from './components/BrowserTabBar'; import BrowserNavBar from './components/BrowserNavBar'; import BrowserActionOverlay from './components/BrowserActionOverlay'; diff --git a/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/BrowserCard/TabLocalState.ts b/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/BrowserCard/TabLocalState.ts new file mode 100644 index 00000000..60e9f51b --- /dev/null +++ b/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/BrowserCard/TabLocalState.ts @@ -0,0 +1,6 @@ +export interface TabLocalState { + loading: boolean; + canGoBack: boolean; + canGoForward: boolean; + } + \ No newline at end of file diff --git a/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/BrowserCard/components/BrowserTabBar.tsx b/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/BrowserCard/components/BrowserTabBar.tsx index 81d89e74..863c1eeb 100644 --- a/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/BrowserCard/components/BrowserTabBar.tsx +++ b/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/BrowserCard/components/BrowserTabBar.tsx @@ -13,7 +13,7 @@ import { reorderBrowserTab, setActiveBrowserTab, addBrowserTab, removeBrowserTab, removeBrowserCard, type BrowserTab, } from '@/shared/state/dashboardLayoutSlice'; -import type { TabLocalState } from '@/app/pages/Dashboard/types/types'; +import type { TabLocalState } from '../TabLocalState'; interface BrowserTabBarProps { tabs: BrowserTab[]; diff --git a/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/BrowserCard/hooks/useWebviewLifecycle.ts b/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/BrowserCard/hooks/useWebviewLifecycle.ts index 5383c5a5..9b57dfe6 100644 --- a/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/BrowserCard/hooks/useWebviewLifecycle.ts +++ b/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/BrowserCard/hooks/useWebviewLifecycle.ts @@ -12,7 +12,7 @@ import { updateBrowserTabFavicon, type BrowserTab, } from '@/shared/state/dashboardLayoutSlice'; -import type { TabLocalState } from '@/app/pages/Dashboard/types/types'; +import type { TabLocalState } from '../TabLocalState'; export type WebviewElement = BrowserWebview; diff --git a/frontend/src/app/pages/Dashboard/DashboardCanvas/components/DashboardToolbar/DashboardToolbar.tsx b/frontend/src/app/pages/Dashboard/DashboardCanvas/components/DashboardToolbar/DashboardToolbar.tsx index af33302f..a266d236 100644 --- a/frontend/src/app/pages/Dashboard/DashboardCanvas/components/DashboardToolbar/DashboardToolbar.tsx +++ b/frontend/src/app/pages/Dashboard/DashboardCanvas/components/DashboardToolbar/DashboardToolbar.tsx @@ -1,11 +1,52 @@ -import React from 'react'; +import React, { useCallback, useRef } from 'react'; import { motion } from 'framer-motion'; -import ChatInput from '@/app/pages/AgentChat/ChatInput/ChatInput'; +import { AssistantRuntimeProvider } from '@assistant-ui/react'; +import OpenSwarmComposer from '@/app/pages/AgentChat/OpenSwarmComposer/OpenSwarmComposer'; +import { useStandaloneComposerRuntime } from '@/app/pages/AgentChat/runtime/useStandaloneComposerRuntime'; +import type { ComposerExtras, DispatchableMessage } from '@/app/pages/AgentChat/runtime/useOpenSwarmRuntime'; import { useDashboardToolbar, TOOLBAR_OWNER_ID, ToolbarProps } from './useDashboardToolbar'; import HistoryPanel from './components/HistoryPanel'; import ViewPickerPanel from './components/ViewPickerPanel'; import ToolbarButtons from './components/ToolbarButtons/ToolbarButtons'; +// TODO: pull this out into a separate file or idk do smthn else but not this +const ToolbarComposer: React.FC<{ + mode: string; onModeChange: (mode: string) => void; + model: string; onModelChange: (model: string) => void; + onSend: ( + message: string, + images?: Array<{ data: string; media_type: string }>, + contextPaths?: Array<{ path: string; type: 'file' | 'directory' }>, + forcedTools?: string[], + attachedSkills?: Array<{ id: string; name: string; content: string }>, + selectedBrowserIds?: string[], + ) => void; +}> = ({ mode, onModeChange, model, onModelChange, onSend }) => { + const composerExtrasRef = useRef({}); + const dispatch = useCallback( + (msg: DispatchableMessage) => { + onSend(msg.prompt, msg.images, msg.contextPaths, msg.forcedTools, msg.attachedSkills, msg.selectedBrowserIds); + }, + [onSend], + ); + const runtime = useStandaloneComposerRuntime(composerExtrasRef, dispatch); + + return ( + + + + ); +}; + const DashboardToolbar = React.forwardRef( ({ inputOpen, onNewAgent, onCancel, onSend, onAddView, onHistoryResume, onAddBrowser, dashboardId }, ref) => { const { @@ -44,15 +85,12 @@ const DashboardToolbar = React.forwardRef( > {inputOpen ? (
-
) : historyOpen ? ( diff --git a/frontend/src/app/pages/Dashboard/types/types.ts b/frontend/src/app/pages/Dashboard/types/types.ts index 74b49cec..5145f600 100644 --- a/frontend/src/app/pages/Dashboard/types/types.ts +++ b/frontend/src/app/pages/Dashboard/types/types.ts @@ -1,9 +1,3 @@ -export interface TabLocalState { - loading: boolean; - canGoBack: boolean; - canGoForward: boolean; -} - export interface TetherInfo { key: string; path: string;