mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-12 20:57:42 +02:00
[Haik]: ckpt (shld have fixed tool mcp discovery in prod - but untested) (Added browser control animations) (Made browser top input function as search) (swapped run.sh to not open up site locally in addition to the electron app)
This commit is contained in:
@@ -32,6 +32,7 @@ import ChatInput, { ChatInputHandle } from './ChatInput';
|
||||
import { ContextPath } from '@/app/components/DirectoryBrowser';
|
||||
import BranchNavigator from './BranchNavigator';
|
||||
import DiffViewer from './DiffViewer';
|
||||
import { setGlowingBrowserCards, clearGlowingBrowserCards } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
|
||||
const CONTEXT_WINDOWS: Record<string, number> = {
|
||||
@@ -162,6 +163,7 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
const curr = session?.status;
|
||||
prevStatusRef.current = curr;
|
||||
if (prev === 'running' && (curr === 'completed' || curr === 'stopped' || curr === 'error')) {
|
||||
if (id) dispatch(clearGlowingBrowserCards(id));
|
||||
const currentMode = modesMap[mode];
|
||||
if (currentMode?.default_next_mode && modesMap[currentMode.default_next_mode]) {
|
||||
setMode(currentMode.default_next_mode);
|
||||
@@ -197,7 +199,7 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
}
|
||||
}, [session?.messages.length, session?.streamingMessage?.content]);
|
||||
|
||||
const handleSend = (prompt: 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 }>) => {
|
||||
const handleSend = (prompt: 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[]) => {
|
||||
if (!id) return;
|
||||
if (isDraft) {
|
||||
const config: Record<string, any> = { model, mode };
|
||||
@@ -207,10 +209,17 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
launchAndSendFirstMessage({ draftId: id, config, prompt, mode, model, images, contextPaths, forcedTools, attachedSkills })
|
||||
).then((action) => {
|
||||
if (launchAndSendFirstMessage.fulfilled.match(action)) {
|
||||
dispatch(generateTitle({ sessionId: action.payload.session.id, prompt }));
|
||||
const realId = action.payload.session.id;
|
||||
dispatch(generateTitle({ sessionId: realId, prompt }));
|
||||
if (selectedBrowserIds?.length) {
|
||||
dispatch(setGlowingBrowserCards({ browserIds: selectedBrowserIds, sessionId: realId }));
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (selectedBrowserIds?.length) {
|
||||
dispatch(setGlowingBrowserCards({ browserIds: selectedBrowserIds, sessionId: id }));
|
||||
}
|
||||
dispatch(sendMessageThunk({ sessionId: id, prompt, mode, model, images, contextPaths, forcedTools, attachedSkills }));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -60,7 +60,7 @@ export interface ForcedToolGroup {
|
||||
export type { AttachedSkill } from '@/app/components/richEditorUtils';
|
||||
|
||||
interface Props {
|
||||
onSend: (message: string, images?: Array<{ data: string; media_type: string }>, contextPaths?: ContextPath[], forcedTools?: string[], attachedSkills?: Array<{ id: string; name: string; content: string }>) => void;
|
||||
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;
|
||||
@@ -364,12 +364,16 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(({ onSend, disabled, mode,
|
||||
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,
|
||||
sendImages,
|
||||
contextPaths.length > 0 ? contextPaths : undefined,
|
||||
allForcedToolNames.length > 0 ? allForcedToolNames : undefined,
|
||||
sendSkills,
|
||||
browserIds.length > 0 ? browserIds : undefined,
|
||||
);
|
||||
editor.innerHTML = '';
|
||||
setImages([]);
|
||||
|
||||
@@ -12,6 +12,7 @@ import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import LockIcon from '@mui/icons-material/Lock';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import SmartToyOutlinedIcon from '@mui/icons-material/SmartToyOutlined';
|
||||
import {
|
||||
setBrowserCardPosition,
|
||||
@@ -19,11 +20,12 @@ import {
|
||||
removeBrowserCard,
|
||||
updateBrowserCardUrl,
|
||||
} from '@/shared/state/dashboardLayoutSlice';
|
||||
import { useAppDispatch } from '@/shared/hooks';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { registerWebview, unregisterWebview, type BrowserWebview } from '@/shared/browserRegistry';
|
||||
import { useBrowserActivity } from '@/shared/useBrowserActivity';
|
||||
import { getActionLabel } from '@/shared/browserCommandHandler';
|
||||
import { resolveInput, isGoogleSearch } from '@/shared/resolveUrl';
|
||||
|
||||
type ResizeDir = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw';
|
||||
|
||||
@@ -68,12 +70,6 @@ interface Props {
|
||||
onDragEnd?: (dx: number, dy: number, didDrag: boolean) => void;
|
||||
}
|
||||
|
||||
function ensureProtocol(input: string): string {
|
||||
const trimmed = input.trim();
|
||||
if (/^https?:\/\//i.test(trimmed)) return trimmed;
|
||||
if (/^[a-zA-Z0-9-]+\.[a-zA-Z]{2,}/.test(trimmed)) return `https://${trimmed}`;
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
const BrowserCard: React.FC<Props> = ({
|
||||
browserId, url, cardX, cardY, cardWidth, cardHeight, zoom = 1,
|
||||
@@ -150,10 +146,12 @@ const BrowserCard: React.FC<Props> = ({
|
||||
}, [browserId]);
|
||||
|
||||
const navigate = useCallback((targetUrl: string) => {
|
||||
const finalUrl = ensureProtocol(targetUrl);
|
||||
const finalUrl = resolveInput(targetUrl);
|
||||
setUrlBarValue(finalUrl);
|
||||
if (isElectron && webviewRef.current) {
|
||||
webviewRef.current.loadURL(finalUrl);
|
||||
webviewRef.current.loadURL(finalUrl).catch((err: Error) => {
|
||||
if (!err.message?.includes('ERR_ABORTED')) console.error('Navigation failed:', err);
|
||||
});
|
||||
}
|
||||
setCurrentUrl(finalUrl);
|
||||
dispatch(updateBrowserCardUrl({ browserId, url: finalUrl }));
|
||||
@@ -310,21 +308,43 @@ const BrowserCard: React.FC<Props> = ({
|
||||
const noTransition = isDragging || isResizing || (isSelected && !!multiDragDelta);
|
||||
|
||||
const isSecure = currentUrl.startsWith('https://');
|
||||
const isSearch = isGoogleSearch(currentUrl);
|
||||
|
||||
const accentColor = c.accent.primary;
|
||||
const accentHover = c.accent.hover;
|
||||
|
||||
const glowingBrowserCards = useAppSelector((s) => s.dashboardLayout.glowingBrowserCards);
|
||||
const isGlowingFromRedux = !!glowingBrowserCards[browserId];
|
||||
|
||||
const [hasBeenTouched, setHasBeenTouched] = useState(false);
|
||||
useEffect(() => {
|
||||
if (isGlowingFromRedux && agentActive) setHasBeenTouched(true);
|
||||
}, [isGlowingFromRedux, agentActive]);
|
||||
useEffect(() => {
|
||||
if (!isGlowingFromRedux) setHasBeenTouched(false);
|
||||
}, [isGlowingFromRedux]);
|
||||
|
||||
const showGlow = isGlowingFromRedux && hasBeenTouched;
|
||||
|
||||
const agentBorder = agentActive
|
||||
? `2px solid ${accentColor}`
|
||||
: isSelected ? '2px solid #3b82f6' : `1px solid ${c.border.medium}`;
|
||||
: showGlow
|
||||
? `2px solid ${accentColor}`
|
||||
: isSelected ? '2px solid #3b82f6' : `1px solid ${c.border.medium}`;
|
||||
|
||||
const innerGlow = showGlow && !agentActive
|
||||
? `, inset 0 0 30px ${accentColor}25, inset 0 0 60px ${accentColor}10`
|
||||
: '';
|
||||
|
||||
const agentShadow = agentActive
|
||||
? `0 0 0 2px ${accentColor}40, 0 0 18px ${accentColor}30, 0 0 40px ${accentColor}15`
|
||||
: isDragging || isResizing
|
||||
? c.shadow.lg
|
||||
: isSelected
|
||||
? `0 0 0 1px #3b82f6, ${c.shadow.md}`
|
||||
: c.shadow.md;
|
||||
: showGlow
|
||||
? `0 0 0 2px ${accentColor}40, 0 0 18px ${accentColor}30, 0 0 40px ${accentColor}15${innerGlow}`
|
||||
: isDragging || isResizing
|
||||
? c.shadow.lg
|
||||
: isSelected
|
||||
? `0 0 0 1px #3b82f6, ${c.shadow.md}`
|
||||
: c.shadow.md;
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -348,22 +368,51 @@ const BrowserCard: React.FC<Props> = ({
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
zIndex: (isDragging || isResizing) ? 100 : agentActive ? 50 : 1,
|
||||
zIndex: (isDragging || isResizing) ? 100 : (agentActive || showGlow) ? 50 : 1,
|
||||
transition: noTransition ? 'none' : 'box-shadow 0.4s ease, border 0.3s ease',
|
||||
'&:hover .resize-handle': { opacity: 1 },
|
||||
...(agentActive && {
|
||||
...((agentActive || showGlow) && {
|
||||
animation: 'agent-glow-pulse 2s ease-in-out infinite',
|
||||
'@keyframes agent-glow-pulse': {
|
||||
'0%, 100%': {
|
||||
boxShadow: `0 0 0 2px ${accentColor}40, 0 0 18px ${accentColor}30, 0 0 40px ${accentColor}15`,
|
||||
boxShadow: `0 0 0 2px ${accentColor}40, 0 0 18px ${accentColor}30, 0 0 40px ${accentColor}15${innerGlow}`,
|
||||
},
|
||||
'50%': {
|
||||
boxShadow: `0 0 0 3px ${accentColor}60, 0 0 28px ${accentColor}45, 0 0 56px ${accentColor}25`,
|
||||
boxShadow: `0 0 0 3px ${accentColor}60, 0 0 28px ${accentColor}45, 0 0 56px ${accentColor}25${innerGlow}`,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{/* Rotating gradient border glow for element selection / streaming */}
|
||||
{showGlow && !agentActive && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
borderRadius: 'inherit',
|
||||
zIndex: 20,
|
||||
pointerEvents: 'none',
|
||||
overflow: 'hidden',
|
||||
padding: '3px',
|
||||
mask: 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)',
|
||||
WebkitMask: 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)',
|
||||
maskComposite: 'exclude',
|
||||
WebkitMaskComposite: 'xor',
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
inset: '-50%',
|
||||
background: `conic-gradient(from 0deg, transparent 0%, ${accentColor} 25%, transparent 50%, ${accentColor} 75%, transparent 100%)`,
|
||||
animation: 'rotate-glow 3s linear infinite',
|
||||
},
|
||||
'@keyframes rotate-glow': {
|
||||
'100%': { transform: 'rotate(360deg)' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Animated border glow (top edge overlay) */}
|
||||
{agentActive && (
|
||||
<Box
|
||||
@@ -523,16 +572,18 @@ const BrowserCard: React.FC<Props> = ({
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{isSecure && (
|
||||
{isSearch ? (
|
||||
<SearchIcon sx={{ fontSize: 14, color: c.text.muted, flexShrink: 0 }} />
|
||||
) : isSecure ? (
|
||||
<LockIcon sx={{ fontSize: 13, color: c.status.success, flexShrink: 0 }} />
|
||||
)}
|
||||
) : null}
|
||||
<InputBase
|
||||
value={urlBarValue}
|
||||
onChange={(e) => setUrlBarValue(e.target.value)}
|
||||
onKeyDown={handleUrlKeyDown}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onFocus={(e) => (e.target as HTMLInputElement).select()}
|
||||
placeholder="Enter URL..."
|
||||
placeholder="Search Google or enter URL..."
|
||||
sx={{
|
||||
flex: 1,
|
||||
fontSize: '0.76rem',
|
||||
@@ -700,6 +751,29 @@ const BrowserCard: React.FC<Props> = ({
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Orange inner shadow overlay for selection / streaming glow */}
|
||||
{showGlow && !agentActive && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
zIndex: 14,
|
||||
pointerEvents: 'none',
|
||||
borderRadius: 'inherit',
|
||||
boxShadow: 'inset 0 0 40px rgba(255,140,0,0.35), inset 0 0 80px rgba(255,100,0,0.15)',
|
||||
animation: 'orange-glow-pulse 2s ease-in-out infinite',
|
||||
'@keyframes orange-glow-pulse': {
|
||||
'0%, 100%': {
|
||||
boxShadow: 'inset 0 0 40px rgba(255,140,0,0.35), inset 0 0 80px rgba(255,100,0,0.15)',
|
||||
},
|
||||
'50%': {
|
||||
boxShadow: 'inset 0 0 50px rgba(255,140,0,0.45), inset 0 0 100px rgba(255,100,0,0.22)',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ===== Frosted glass overlay ===== */}
|
||||
{agentActive && (
|
||||
<Box
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
addBrowserCard,
|
||||
moveCards,
|
||||
resetLayout,
|
||||
setGlowingBrowserCards,
|
||||
} from '@/shared/state/dashboardLayoutSlice';
|
||||
import { fetchOutputs } from '@/shared/state/outputsSlice';
|
||||
import { generateDashboardName } from '@/shared/state/dashboardsSlice';
|
||||
@@ -253,6 +254,7 @@ const DashboardInner: React.FC = () => {
|
||||
contextPaths?: ContextPath[],
|
||||
forcedTools?: string[],
|
||||
attachedSkills?: Array<{ id: string; name: string; content: string }>,
|
||||
selectedBrowserIds?: string[],
|
||||
) => {
|
||||
setToolbarOpen(false);
|
||||
|
||||
@@ -290,6 +292,9 @@ const DashboardInner: React.FC = () => {
|
||||
if (launchAndSendFirstMessage.fulfilled.match(action)) {
|
||||
const realId = action.payload.session.id;
|
||||
dispatch(generateTitle({ sessionId: realId, prompt }));
|
||||
if (selectedBrowserIds?.length) {
|
||||
dispatch(setGlowingBrowserCards({ browserIds: selectedBrowserIds, sessionId: realId }));
|
||||
}
|
||||
spawnOriginsRef.current[realId] = spawnOriginsRef.current[draftId];
|
||||
delete spawnOriginsRef.current[draftId];
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ interface Props {
|
||||
contextPaths?: ContextPath[],
|
||||
forcedTools?: string[],
|
||||
attachedSkills?: Array<{ id: string; name: string; content: string }>,
|
||||
selectedBrowserIds?: string[],
|
||||
) => void;
|
||||
onAddView: (outputId: string) => void;
|
||||
onHistoryResume: (sessionId: string) => void;
|
||||
@@ -128,8 +129,9 @@ const DashboardToolbar = React.forwardRef<HTMLDivElement, Props>(
|
||||
contextPaths?: ContextPath[],
|
||||
forcedTools?: string[],
|
||||
attachedSkills?: Array<{ id: string; name: string; content: string }>,
|
||||
selectedBrowserIds?: string[],
|
||||
) => {
|
||||
onSend(message, mode, model, images, contextPaths, forcedTools, attachedSkills);
|
||||
onSend(message, mode, model, images, contextPaths, forcedTools, attachedSkills, selectedBrowserIds);
|
||||
},
|
||||
[onSend, mode, model],
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getWebview, type BrowserWebview } from './browserRegistry';
|
||||
import { dashboardWs } from './ws/WebSocketManager';
|
||||
import { resolveInput } from './resolveUrl';
|
||||
|
||||
let initialized = false;
|
||||
|
||||
@@ -61,9 +62,14 @@ async function handleGetText(wv: BrowserWebview): Promise<Record<string, any>> {
|
||||
}
|
||||
|
||||
async function handleNavigate(wv: BrowserWebview, params: Record<string, any>): Promise<Record<string, any>> {
|
||||
const url = params.url as string;
|
||||
if (!url) return { error: 'url parameter is required' };
|
||||
await wv.loadURL(url);
|
||||
const raw = params.url as string;
|
||||
if (!raw) return { error: 'url parameter is required' };
|
||||
const url = resolveInput(raw);
|
||||
try {
|
||||
await wv.loadURL(url);
|
||||
} catch (err: any) {
|
||||
if (!err?.message?.includes('ERR_ABORTED')) throw err;
|
||||
}
|
||||
return { text: `Navigated to ${url}`, url };
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Resolves raw URL-bar input into a navigable URL.
|
||||
*
|
||||
* Priority:
|
||||
* 1. Already has a scheme (http://, https://, file://, etc.) → pass through
|
||||
* 2. Starts with / or ~ → file path, prefix with file://
|
||||
* 3. localhost (with optional port/path) → http://
|
||||
* 4. IP address (with optional port/path) → http://
|
||||
* 5. No spaces + contains a dot followed by a 2+ char TLD → domain, prefix https://
|
||||
* 6. Everything else → Google search
|
||||
*/
|
||||
export function resolveInput(input: string): string {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) return trimmed;
|
||||
|
||||
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//i.test(trimmed)) return trimmed;
|
||||
if (/^[~/]/.test(trimmed)) return `file://${trimmed}`;
|
||||
if (/^localhost(:\d+)?(\/.*)?$/i.test(trimmed)) return `http://${trimmed}`;
|
||||
if (/^\d{1,3}(\.\d{1,3}){3}(:\d+)?(\/.*)?$/.test(trimmed)) return `http://${trimmed}`;
|
||||
if (!/\s/.test(trimmed) && /\.[a-zA-Z]{2,}/.test(trimmed)) return `https://${trimmed}`;
|
||||
|
||||
return `https://www.google.com/search?q=${encodeURIComponent(trimmed)}`;
|
||||
}
|
||||
|
||||
export function isGoogleSearch(url: string): boolean {
|
||||
return url.startsWith('https://www.google.com/search');
|
||||
}
|
||||
@@ -43,6 +43,7 @@ export interface DashboardLayoutState {
|
||||
cards: Record<string, CardPosition>;
|
||||
viewCards: Record<string, ViewCardPosition>;
|
||||
browserCards: Record<string, BrowserCardPosition>;
|
||||
glowingBrowserCards: Record<string, string>;
|
||||
persistedExpandedSessionIds: string[];
|
||||
loading: boolean;
|
||||
initialized: boolean;
|
||||
@@ -52,6 +53,7 @@ const initialState: DashboardLayoutState = {
|
||||
cards: {},
|
||||
viewCards: {},
|
||||
browserCards: {},
|
||||
glowingBrowserCards: {},
|
||||
persistedExpandedSessionIds: [],
|
||||
loading: false,
|
||||
initialized: false,
|
||||
@@ -384,10 +386,32 @@ const dashboardLayoutSlice = createSlice({
|
||||
}
|
||||
},
|
||||
|
||||
setGlowingBrowserCards(
|
||||
state,
|
||||
action: PayloadAction<{ browserIds: string[]; sessionId: string }>
|
||||
) {
|
||||
const { browserIds, sessionId } = action.payload;
|
||||
for (const id of browserIds) {
|
||||
state.glowingBrowserCards[id] = sessionId;
|
||||
}
|
||||
},
|
||||
|
||||
clearGlowingBrowserCards(state, action: PayloadAction<string>) {
|
||||
const sessionId = action.payload;
|
||||
for (const [browserId, sid] of Object.entries(state.glowingBrowserCards)) {
|
||||
if (sid === sessionId) delete state.glowingBrowserCards[browserId];
|
||||
}
|
||||
},
|
||||
|
||||
clearAllGlowingBrowserCards(state) {
|
||||
state.glowingBrowserCards = {};
|
||||
},
|
||||
|
||||
resetLayout(state) {
|
||||
state.cards = {};
|
||||
state.viewCards = {};
|
||||
state.browserCards = {};
|
||||
state.glowingBrowserCards = {};
|
||||
state.persistedExpandedSessionIds = [];
|
||||
state.initialized = false;
|
||||
},
|
||||
@@ -438,6 +462,9 @@ export const {
|
||||
removeBrowserCard,
|
||||
updateBrowserCardUrl,
|
||||
moveCards,
|
||||
setGlowingBrowserCards,
|
||||
clearGlowingBrowserCards,
|
||||
clearAllGlowingBrowserCards,
|
||||
resetLayout,
|
||||
} = dashboardLayoutSlice.actions;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user