mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[Haik]: (fixed auto spawn glitch on reload) (also swapped notification ui to be a floating island)
This commit is contained in:
@@ -0,0 +1,732 @@
|
||||
import React, { useMemo, useCallback, useState, useEffect, useRef } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import StopCircleOutlinedIcon from '@mui/icons-material/StopCircleOutlined';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import {
|
||||
handleApproval,
|
||||
stopAgent,
|
||||
dismissAgentNotification,
|
||||
ApprovalRequest,
|
||||
AgentSession,
|
||||
HistorySession,
|
||||
} from '@/shared/state/agentsSlice';
|
||||
import { setPendingFocusAgentId } from '@/shared/state/tempStateSlice';
|
||||
import ApprovalBar, { BatchApprovalBar } from '@/app/pages/AgentChat/ApprovalBar';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type IslandState = 'idle' | 'compact' | 'expanded';
|
||||
|
||||
interface SessionApprovalGroup {
|
||||
sessionId: string;
|
||||
sessionName: string;
|
||||
approvals: ApprovalRequest[];
|
||||
}
|
||||
|
||||
type TrackedAgent = {
|
||||
id: string;
|
||||
name: string;
|
||||
status: AgentSession['status'] | string;
|
||||
dashboardId?: string;
|
||||
};
|
||||
|
||||
const STATUS_CONFIG: Record<string, { label: string; tokenKey?: string }> = {
|
||||
running: { label: 'Running', tokenKey: 'success' },
|
||||
waiting_approval: { label: 'Waiting', tokenKey: 'warning' },
|
||||
completed: { label: 'Done', tokenKey: 'success' },
|
||||
error: { label: 'Error', tokenKey: 'error' },
|
||||
stopped: { label: 'Stopped', tokenKey: 'info' },
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Spring configs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const SPRING_LAYOUT = { type: 'spring' as const, stiffness: 400, damping: 30 };
|
||||
const SPRING_BOUNCE = { type: 'spring' as const, stiffness: 500, damping: 25 };
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sub-components
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const StatusDot: React.FC<{ status: string; c: ReturnType<typeof useClaudeTokens> }> = ({ status, c }) => {
|
||||
const cfg = STATUS_CONFIG[status];
|
||||
const color = cfg?.tokenKey ? (c.status as any)[cfg.tokenKey] : c.text.ghost;
|
||||
const isActive = status === 'running';
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: '50%',
|
||||
bgcolor: color,
|
||||
flexShrink: 0,
|
||||
opacity: 0.8,
|
||||
...(isActive && {
|
||||
animation: 'islandPulse 2s ease-in-out infinite',
|
||||
'@keyframes islandPulse': {
|
||||
'0%, 100%': { opacity: 0.8, transform: 'scale(1)' },
|
||||
'50%': { opacity: 0.4, transform: 'scale(1.3)' },
|
||||
},
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const AgentStatusRow: React.FC<{
|
||||
agent: TrackedAgent;
|
||||
c: ReturnType<typeof useClaudeTokens>;
|
||||
onStop: (id: string) => void;
|
||||
onDismiss: (id: string) => void;
|
||||
onNavigate: (dashboardId: string, agentId: string) => void;
|
||||
}> = ({ agent, c, onStop, onDismiss, onNavigate }) => {
|
||||
const isActive = agent.status === 'running' || agent.status === 'waiting_approval';
|
||||
const cfg = STATUS_CONFIG[agent.status] ?? { label: agent.status };
|
||||
|
||||
return (
|
||||
<Box
|
||||
onClick={() => agent.dashboardId && onNavigate(agent.dashboardId, agent.id)}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
px: 2,
|
||||
py: 0.75,
|
||||
cursor: agent.dashboardId ? 'pointer' : 'default',
|
||||
'&:hover': { bgcolor: c.border.subtle },
|
||||
transition: 'background-color 0.15s',
|
||||
minHeight: 34,
|
||||
}}
|
||||
>
|
||||
<StatusDot status={agent.status} c={c} />
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.78rem',
|
||||
fontWeight: 500,
|
||||
color: c.text.secondary,
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{agent.name}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.6rem',
|
||||
color: c.text.ghost,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.04em',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{cfg.label}
|
||||
</Typography>
|
||||
{isActive ? (
|
||||
<Tooltip title="Stop agent" arrow>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => { e.stopPropagation(); onStop(agent.id); }}
|
||||
sx={{ p: 0.25, color: c.text.ghost, '&:hover': { color: c.status.error, bgcolor: c.border.subtle } }}
|
||||
>
|
||||
<StopCircleOutlinedIcon sx={{ fontSize: 15 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title="Dismiss" arrow>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => { e.stopPropagation(); onDismiss(agent.id); }}
|
||||
sx={{ p: 0.25, color: c.text.ghost, '&:hover': { bgcolor: c.border.subtle } }}
|
||||
>
|
||||
<CloseIcon sx={{ fontSize: 13 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Compact activity indicator — subtle breathing dot
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const ActivityIndicator: React.FC<{ c: ReturnType<typeof useClaudeTokens> }> = ({ c }) => (
|
||||
<Box
|
||||
sx={{
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: '50%',
|
||||
bgcolor: c.text.tertiary,
|
||||
flexShrink: 0,
|
||||
animation: 'subtlePulse 2.2s ease-in-out infinite',
|
||||
'@keyframes subtlePulse': {
|
||||
'0%, 100%': { opacity: 0.6, transform: 'scale(1)' },
|
||||
'50%': { opacity: 1, transform: 'scale(1.15)' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const DynamicIsland: React.FC = () => {
|
||||
const c = useClaudeTokens();
|
||||
const dispatch = useAppDispatch();
|
||||
const navigate = useNavigate();
|
||||
const islandRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const sessions = useAppSelector((state) => state.agents.sessions);
|
||||
const history = useAppSelector((state) => state.agents.history);
|
||||
const trackedIds = useAppSelector((state) => state.agents.trackedNotificationIds);
|
||||
|
||||
const [userExpanded, setUserExpanded] = useState(false);
|
||||
|
||||
// ---- Derived data ----
|
||||
|
||||
const groups: SessionApprovalGroup[] = useMemo(() => {
|
||||
const result: SessionApprovalGroup[] = [];
|
||||
for (const [sessionId, session] of Object.entries(sessions)) {
|
||||
if (session.pending_approvals?.length > 0) {
|
||||
result.push({
|
||||
sessionId,
|
||||
sessionName: session.name || 'Agent',
|
||||
approvals: session.pending_approvals,
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}, [sessions]);
|
||||
|
||||
const totalApprovals = useMemo(
|
||||
() => groups.reduce((sum, g) => sum + g.approvals.length, 0),
|
||||
[groups],
|
||||
);
|
||||
|
||||
const trackedAgents: TrackedAgent[] = useMemo(() => {
|
||||
return trackedIds
|
||||
.map((id): TrackedAgent | null => {
|
||||
const session = sessions[id];
|
||||
if (session && session.status !== 'draft') {
|
||||
return { id, name: session.name, status: session.status, dashboardId: session.dashboard_id };
|
||||
}
|
||||
const hist: HistorySession | undefined = history[id];
|
||||
if (hist) {
|
||||
return { id, name: hist.name, status: hist.status, dashboardId: hist.dashboard_id };
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((a): a is TrackedAgent => a !== null);
|
||||
}, [trackedIds, sessions, history]);
|
||||
|
||||
const activeAgents = useMemo(
|
||||
() => trackedAgents.filter((a) => a.status === 'running' || a.status === 'waiting_approval'),
|
||||
[trackedAgents],
|
||||
);
|
||||
const finishedAgents = useMemo(
|
||||
() => trackedAgents.filter((a) => a.status !== 'running' && a.status !== 'waiting_approval'),
|
||||
[trackedAgents],
|
||||
);
|
||||
|
||||
const hasApprovals = totalApprovals > 0;
|
||||
const hasAgents = trackedAgents.length > 0;
|
||||
|
||||
// ---- Island state machine ----
|
||||
|
||||
const islandState: IslandState = useMemo(() => {
|
||||
if (hasApprovals) return 'expanded';
|
||||
if (userExpanded && hasAgents) return 'expanded';
|
||||
if (hasAgents) return 'compact';
|
||||
return 'idle';
|
||||
}, [hasApprovals, userExpanded, hasAgents]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasAgents && !hasApprovals) {
|
||||
setUserExpanded(false);
|
||||
}
|
||||
}, [hasAgents, hasApprovals]);
|
||||
|
||||
// ---- Click outside to collapse ----
|
||||
|
||||
useEffect(() => {
|
||||
if (islandState !== 'expanded') return;
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (islandRef.current && !islandRef.current.contains(e.target as Node)) {
|
||||
if (!hasApprovals) setUserExpanded(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handler);
|
||||
return () => document.removeEventListener('mousedown', handler);
|
||||
}, [islandState, hasApprovals]);
|
||||
|
||||
// ---- Callbacks ----
|
||||
|
||||
const onApprove = useCallback(
|
||||
(requestId: string, updatedInput?: Record<string, any>) => {
|
||||
dispatch(handleApproval({ requestId, behavior: 'allow', updatedInput }));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const onDeny = useCallback(
|
||||
(requestId: string, message?: string) => {
|
||||
dispatch(handleApproval({ requestId, behavior: 'deny', message }));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const onStopAgent = useCallback(
|
||||
(sessionId: string) => dispatch(stopAgent({ sessionId })),
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const onDismissAgent = useCallback(
|
||||
(sessionId: string) => dispatch(dismissAgentNotification(sessionId)),
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const onNavigateToDashboard = useCallback(
|
||||
(dashboardId: string, agentId: string) => {
|
||||
dispatch(setPendingFocusAgentId(agentId));
|
||||
navigate(`/dashboard/${dashboardId}`);
|
||||
},
|
||||
[navigate, dispatch],
|
||||
);
|
||||
|
||||
const handleIslandClick = useCallback(() => {
|
||||
if (islandState === 'compact') {
|
||||
setUserExpanded(true);
|
||||
} else if (islandState === 'expanded' && !hasApprovals) {
|
||||
setUserExpanded(false);
|
||||
}
|
||||
}, [islandState, hasApprovals]);
|
||||
|
||||
// ---- Styling — uses the same neutral palette as the rest of the UI ----
|
||||
|
||||
const islandWidth = islandState === 'idle'
|
||||
? 126
|
||||
: islandState === 'compact'
|
||||
? 220
|
||||
: 400;
|
||||
|
||||
const islandBorderRadius = islandState === 'expanded' ? 14 : 50;
|
||||
|
||||
const shadow = islandState === 'idle'
|
||||
? 'none'
|
||||
: islandState === 'compact'
|
||||
? c.shadow.sm
|
||||
: c.shadow.md;
|
||||
|
||||
// ---- Compact summary text ----
|
||||
|
||||
const compactText = useMemo(() => {
|
||||
const parts: string[] = [];
|
||||
if (activeAgents.length > 0) {
|
||||
parts.push(`${activeAgents.length} running`);
|
||||
}
|
||||
if (finishedAgents.length > 0) {
|
||||
parts.push(`${finishedAgents.length} done`);
|
||||
}
|
||||
return parts.join(' · ') || 'Agents';
|
||||
}, [activeAgents.length, finishedAgents.length]);
|
||||
|
||||
// ---- Render ----
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={islandRef}
|
||||
layout
|
||||
transition={islandState === 'expanded' ? SPRING_LAYOUT : SPRING_BOUNCE}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
top: 6,
|
||||
x: '-50%',
|
||||
zIndex: 9999,
|
||||
width: islandWidth,
|
||||
borderRadius: islandBorderRadius,
|
||||
cursor: islandState === 'expanded' ? 'default' : 'pointer',
|
||||
// @ts-expect-error -- vendor prefix
|
||||
WebkitAppRegion: 'no-drag',
|
||||
}}
|
||||
onClick={islandState !== 'expanded' ? handleIslandClick : undefined}
|
||||
>
|
||||
<motion.div
|
||||
layout
|
||||
transition={SPRING_LAYOUT}
|
||||
style={{
|
||||
background: c.bg.secondary,
|
||||
border: `0.5px solid ${c.border.medium}`,
|
||||
borderRadius: islandBorderRadius,
|
||||
boxShadow: shadow,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<AnimatePresence mode="wait">
|
||||
{islandState === 'idle' && (
|
||||
<IdlePill key="idle" c={c} />
|
||||
)}
|
||||
{islandState === 'compact' && (
|
||||
<CompactPill
|
||||
key="compact"
|
||||
c={c}
|
||||
text={compactText}
|
||||
activeCount={activeAgents.length}
|
||||
hasApprovals={hasApprovals}
|
||||
/>
|
||||
)}
|
||||
{islandState === 'expanded' && (
|
||||
<ExpandedCard
|
||||
key="expanded"
|
||||
c={c}
|
||||
groups={groups}
|
||||
totalApprovals={totalApprovals}
|
||||
activeAgents={activeAgents}
|
||||
finishedAgents={finishedAgents}
|
||||
hasApprovals={hasApprovals}
|
||||
hasAgents={hasAgents}
|
||||
onApprove={onApprove}
|
||||
onDeny={onDeny}
|
||||
onStopAgent={onStopAgent}
|
||||
onDismissAgent={onDismissAgent}
|
||||
onNavigateToDashboard={onNavigateToDashboard}
|
||||
onCollapse={() => setUserExpanded(false)}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Idle pill
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const IdlePill: React.FC<{ c: ReturnType<typeof useClaudeTokens> }> = ({ c }) => (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.92 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.92 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<motion.div
|
||||
animate={{ scale: [1, 1.006, 1] }}
|
||||
transition={{ repeat: Infinity, duration: 4, ease: 'easeInOut' }}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 0.6,
|
||||
px: 1.5,
|
||||
height: 24,
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="img"
|
||||
src="./logo.png"
|
||||
alt="OpenSwarm"
|
||||
sx={{ width: 13, height: 13, borderRadius: 0.5, opacity: 0.6 }}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
color: c.text.tertiary,
|
||||
fontSize: '0.68rem',
|
||||
fontWeight: 500,
|
||||
letterSpacing: 0.3,
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
OpenSwarm
|
||||
</Typography>
|
||||
</Box>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Compact pill
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const CompactPill: React.FC<{
|
||||
c: ReturnType<typeof useClaudeTokens>;
|
||||
text: string;
|
||||
activeCount: number;
|
||||
hasApprovals: boolean;
|
||||
}> = ({ c, text, activeCount, hasApprovals }) => (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.92 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.92 }}
|
||||
transition={SPRING_BOUNCE}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.75,
|
||||
px: 1.5,
|
||||
height: 24,
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
<ActivityIndicator c={c} />
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.68rem',
|
||||
fontWeight: 500,
|
||||
color: c.text.tertiary,
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</Typography>
|
||||
{hasApprovals && (
|
||||
<Box
|
||||
sx={{
|
||||
width: 4,
|
||||
height: 4,
|
||||
borderRadius: '50%',
|
||||
bgcolor: c.accent.primary,
|
||||
flexShrink: 0,
|
||||
opacity: 0.8,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Box
|
||||
component="img"
|
||||
src="./logo.png"
|
||||
alt=""
|
||||
sx={{ width: 11, height: 11, borderRadius: 0.25, opacity: 0.45, flexShrink: 0 }}
|
||||
/>
|
||||
</Box>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Expanded card
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const ExpandedCard: React.FC<{
|
||||
c: ReturnType<typeof useClaudeTokens>;
|
||||
groups: SessionApprovalGroup[];
|
||||
totalApprovals: number;
|
||||
activeAgents: TrackedAgent[];
|
||||
finishedAgents: TrackedAgent[];
|
||||
hasApprovals: boolean;
|
||||
hasAgents: boolean;
|
||||
onApprove: (requestId: string, updatedInput?: Record<string, any>) => void;
|
||||
onDeny: (requestId: string, message?: string) => void;
|
||||
onStopAgent: (id: string) => void;
|
||||
onDismissAgent: (id: string) => void;
|
||||
onNavigateToDashboard: (dashboardId: string, agentId: string) => void;
|
||||
onCollapse: () => void;
|
||||
}> = ({
|
||||
c, groups, totalApprovals,
|
||||
activeAgents, finishedAgents, hasApprovals, hasAgents,
|
||||
onApprove, onDeny, onStopAgent, onDismissAgent, onNavigateToDashboard, onCollapse,
|
||||
}) => {
|
||||
const headerTitle = hasApprovals && !hasAgents
|
||||
? 'Approval Required'
|
||||
: hasAgents && !hasApprovals
|
||||
? 'Agents'
|
||||
: 'Notifications';
|
||||
|
||||
const badgeCount = totalApprovals + activeAgents.length;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.96 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.96 }}
|
||||
transition={{ duration: 0.18 }}
|
||||
>
|
||||
{/* Header */}
|
||||
<Box
|
||||
onClick={!hasApprovals ? onCollapse : undefined}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
px: 2,
|
||||
py: 1,
|
||||
cursor: hasApprovals ? 'default' : 'pointer',
|
||||
userSelect: 'none',
|
||||
borderBottom: `0.5px solid ${c.border.subtle}`,
|
||||
'&:hover': !hasApprovals ? { bgcolor: c.border.subtle } : {},
|
||||
transition: 'background-color 0.15s',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.76rem',
|
||||
fontWeight: 600,
|
||||
color: c.text.muted,
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{headerTitle}
|
||||
</Typography>
|
||||
{badgeCount > 0 && (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.65rem',
|
||||
fontWeight: 600,
|
||||
color: c.text.ghost,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{badgeCount}
|
||||
</Typography>
|
||||
)}
|
||||
{!hasApprovals && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => { e.stopPropagation(); onCollapse(); }}
|
||||
sx={{ p: 0.25, color: c.text.ghost, '&:hover': { color: c.text.tertiary } }}
|
||||
>
|
||||
<CloseIcon sx={{ fontSize: 13 }} />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Content */}
|
||||
<Box
|
||||
sx={{
|
||||
overflow: 'auto',
|
||||
maxHeight: 'calc(100vh - 100px)',
|
||||
'&::-webkit-scrollbar': { width: 4 },
|
||||
'&::-webkit-scrollbar-track': { background: 'transparent' },
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: c.border.medium,
|
||||
borderRadius: 3,
|
||||
'&:hover': { background: c.border.strong },
|
||||
},
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: `${c.border.medium} transparent`,
|
||||
}}
|
||||
>
|
||||
{hasApprovals && (
|
||||
<Box sx={{ py: 1 }}>
|
||||
{hasAgents && (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.58rem',
|
||||
fontWeight: 600,
|
||||
color: c.text.ghost,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.06em',
|
||||
px: 2,
|
||||
pb: 0.5,
|
||||
}}
|
||||
>
|
||||
Approvals
|
||||
</Typography>
|
||||
)}
|
||||
{groups.map((group) => (
|
||||
<Box key={group.sessionId} sx={{ mb: 1, '&:last-child': { mb: 0 } }}>
|
||||
{groups.length > 1 && (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.65rem',
|
||||
fontWeight: 600,
|
||||
color: c.text.ghost,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.04em',
|
||||
px: 2,
|
||||
py: 0.5,
|
||||
}}
|
||||
>
|
||||
{group.sessionName}
|
||||
</Typography>
|
||||
)}
|
||||
{group.approvals.length > 1 ? (
|
||||
<BatchApprovalBar
|
||||
requests={group.approvals}
|
||||
onApprove={onApprove}
|
||||
onDeny={onDeny}
|
||||
/>
|
||||
) : (
|
||||
group.approvals.map((req) => (
|
||||
<ApprovalBar
|
||||
key={req.id}
|
||||
request={req}
|
||||
onApprove={onApprove}
|
||||
onDeny={onDeny}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{hasApprovals && hasAgents && (
|
||||
<Box sx={{ mx: 2, borderTop: `0.5px solid ${c.border.subtle}` }} />
|
||||
)}
|
||||
|
||||
{hasAgents && (
|
||||
<Box sx={{ py: 0.75 }}>
|
||||
{hasApprovals && (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.58rem',
|
||||
fontWeight: 600,
|
||||
color: c.text.ghost,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.06em',
|
||||
px: 2,
|
||||
pb: 0.5,
|
||||
pt: 0.25,
|
||||
}}
|
||||
>
|
||||
Agents
|
||||
</Typography>
|
||||
)}
|
||||
{activeAgents.map((agent) => (
|
||||
<AgentStatusRow
|
||||
key={agent.id}
|
||||
agent={agent}
|
||||
c={c}
|
||||
onStop={onStopAgent}
|
||||
onDismiss={onDismissAgent}
|
||||
onNavigate={onNavigateToDashboard}
|
||||
/>
|
||||
))}
|
||||
{finishedAgents.map((agent) => (
|
||||
<AgentStatusRow
|
||||
key={agent.id}
|
||||
agent={agent}
|
||||
c={c}
|
||||
onStop={onStopAgent}
|
||||
onDismiss={onDismissAgent}
|
||||
onNavigate={onNavigateToDashboard}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DynamicIsland;
|
||||
@@ -1,459 +0,0 @@
|
||||
import React, { useMemo, useCallback, useState, useEffect } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import NotificationsActiveIcon from '@mui/icons-material/NotificationsActive';
|
||||
import StopCircleOutlinedIcon from '@mui/icons-material/StopCircleOutlined';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import {
|
||||
handleApproval,
|
||||
stopAgent,
|
||||
dismissAgentNotification,
|
||||
ApprovalRequest,
|
||||
AgentSession,
|
||||
HistorySession,
|
||||
} from '@/shared/state/agentsSlice';
|
||||
import { setPendingFocusAgentId } from '@/shared/state/tempStateSlice';
|
||||
import ApprovalBar, { BatchApprovalBar } from '@/app/pages/AgentChat/ApprovalBar';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
|
||||
interface SessionApprovalGroup {
|
||||
sessionId: string;
|
||||
sessionName: string;
|
||||
approvals: ApprovalRequest[];
|
||||
}
|
||||
|
||||
type TrackedAgent = {
|
||||
id: string;
|
||||
name: string;
|
||||
status: AgentSession['status'] | string;
|
||||
dashboardId?: string;
|
||||
};
|
||||
|
||||
const STATUS_CONFIG: Record<string, { color: string; label: string; tokenKey?: string }> = {
|
||||
running: { color: '', label: 'Running', tokenKey: 'success' },
|
||||
waiting_approval: { color: '', label: 'Waiting', tokenKey: 'warning' },
|
||||
completed: { color: '', label: 'Done', tokenKey: 'success' },
|
||||
error: { color: '', label: 'Error', tokenKey: 'error' },
|
||||
stopped: { color: '', label: 'Stopped', tokenKey: 'info' },
|
||||
};
|
||||
|
||||
const StatusDot: React.FC<{ status: string; c: ReturnType<typeof useClaudeTokens> }> = ({ status, c }) => {
|
||||
const cfg = STATUS_CONFIG[status];
|
||||
const color = cfg?.tokenKey ? (c.status as any)[cfg.tokenKey] : c.text.ghost;
|
||||
const isActive = status === 'running';
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
bgcolor: color,
|
||||
flexShrink: 0,
|
||||
...(isActive && {
|
||||
animation: 'agentPulse 1.8s ease-in-out infinite',
|
||||
'@keyframes agentPulse': {
|
||||
'0%, 100%': { opacity: 1, transform: 'scale(1)' },
|
||||
'50%': { opacity: 0.5, transform: 'scale(1.3)' },
|
||||
},
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const AgentStatusRow: React.FC<{
|
||||
agent: TrackedAgent;
|
||||
c: ReturnType<typeof useClaudeTokens>;
|
||||
onStop: (id: string) => void;
|
||||
onDismiss: (id: string) => void;
|
||||
onNavigate: (dashboardId: string, agentId: string) => void;
|
||||
}> = ({ agent, c, onStop, onDismiss, onNavigate }) => {
|
||||
const isActive = agent.status === 'running' || agent.status === 'waiting_approval';
|
||||
const cfg = STATUS_CONFIG[agent.status] ?? { label: agent.status };
|
||||
|
||||
return (
|
||||
<Box
|
||||
onClick={() => agent.dashboardId && onNavigate(agent.dashboardId, agent.id)}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
px: 2,
|
||||
py: 0.75,
|
||||
cursor: agent.dashboardId ? 'pointer' : 'default',
|
||||
'&:hover': { bgcolor: `${c.text.ghost}10` },
|
||||
transition: 'background-color 0.15s',
|
||||
minHeight: 36,
|
||||
}}
|
||||
>
|
||||
<StatusDot status={agent.status} c={c} />
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.8rem',
|
||||
fontWeight: 500,
|
||||
color: c.text.primary,
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{agent.name}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.65rem',
|
||||
color: c.text.ghost,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.03em',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{cfg.label}
|
||||
</Typography>
|
||||
{isActive ? (
|
||||
<Tooltip title="Stop agent" arrow>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => { e.stopPropagation(); onStop(agent.id); }}
|
||||
sx={{ p: 0.25, color: c.status.error, '&:hover': { bgcolor: `${c.status.error}15` } }}
|
||||
>
|
||||
<StopCircleOutlinedIcon sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title="Dismiss" arrow>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => { e.stopPropagation(); onDismiss(agent.id); }}
|
||||
sx={{ p: 0.25, color: c.text.ghost, '&:hover': { bgcolor: `${c.text.ghost}15` } }}
|
||||
>
|
||||
<CloseIcon sx={{ fontSize: 14 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const GlobalApprovalOverlay: React.FC = () => {
|
||||
const c = useClaudeTokens();
|
||||
const dispatch = useAppDispatch();
|
||||
const navigate = useNavigate();
|
||||
const sessions = useAppSelector((state) => state.agents.sessions);
|
||||
const history = useAppSelector((state) => state.agents.history);
|
||||
const trackedIds = useAppSelector((state) => state.agents.trackedNotificationIds);
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
const groups: SessionApprovalGroup[] = useMemo(() => {
|
||||
const result: SessionApprovalGroup[] = [];
|
||||
for (const [sessionId, session] of Object.entries(sessions)) {
|
||||
if (session.pending_approvals?.length > 0) {
|
||||
result.push({
|
||||
sessionId,
|
||||
sessionName: session.name || 'Agent',
|
||||
approvals: session.pending_approvals,
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}, [sessions]);
|
||||
|
||||
const totalApprovals = useMemo(
|
||||
() => groups.reduce((sum, g) => sum + g.approvals.length, 0),
|
||||
[groups],
|
||||
);
|
||||
|
||||
const trackedAgents: TrackedAgent[] = useMemo(() => {
|
||||
return trackedIds
|
||||
.map((id): TrackedAgent | null => {
|
||||
const session = sessions[id];
|
||||
if (session && session.status !== 'draft') {
|
||||
return { id, name: session.name, status: session.status, dashboardId: session.dashboard_id };
|
||||
}
|
||||
const hist: HistorySession | undefined = history[id];
|
||||
if (hist) {
|
||||
return { id, name: hist.name, status: hist.status, dashboardId: hist.dashboard_id };
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((a): a is TrackedAgent => a !== null);
|
||||
}, [trackedIds, sessions, history]);
|
||||
|
||||
const activeAgents = useMemo(
|
||||
() => trackedAgents.filter((a) => a.status === 'running' || a.status === 'waiting_approval'),
|
||||
[trackedAgents],
|
||||
);
|
||||
const finishedAgents = useMemo(
|
||||
() => trackedAgents.filter((a) => a.status !== 'running' && a.status !== 'waiting_approval'),
|
||||
[trackedAgents],
|
||||
);
|
||||
|
||||
const totalBadge = totalApprovals + activeAgents.length;
|
||||
|
||||
useEffect(() => {
|
||||
if (totalApprovals > 0 || activeAgents.length > 0) {
|
||||
setCollapsed(false);
|
||||
}
|
||||
}, [totalApprovals, activeAgents.length]);
|
||||
|
||||
const onApprove = useCallback(
|
||||
(requestId: string, updatedInput?: Record<string, any>) => {
|
||||
dispatch(handleApproval({ requestId, behavior: 'allow', updatedInput }));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const onDeny = useCallback(
|
||||
(requestId: string, message?: string) => {
|
||||
dispatch(handleApproval({ requestId, behavior: 'deny', message }));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const onStopAgent = useCallback(
|
||||
(sessionId: string) => {
|
||||
dispatch(stopAgent({ sessionId }));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const onDismissAgent = useCallback(
|
||||
(sessionId: string) => {
|
||||
dispatch(dismissAgentNotification(sessionId));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const onNavigateToDashboard = useCallback(
|
||||
(dashboardId: string, agentId: string) => {
|
||||
dispatch(setPendingFocusAgentId(agentId));
|
||||
navigate(`/dashboard/${dashboardId}`);
|
||||
},
|
||||
[navigate, dispatch],
|
||||
);
|
||||
|
||||
if (totalApprovals === 0 && trackedAgents.length === 0) return null;
|
||||
|
||||
const hasApprovals = totalApprovals > 0;
|
||||
const hasAgents = trackedAgents.length > 0;
|
||||
const headerTitle = hasApprovals && !hasAgents
|
||||
? 'Approval Required'
|
||||
: hasAgents && !hasApprovals
|
||||
? 'Agents'
|
||||
: 'Notifications';
|
||||
const headerColor = hasApprovals ? c.status.warning : c.status.info;
|
||||
const headerBg = hasApprovals ? c.status.warningBg : c.status.infoBg;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
top: 16,
|
||||
right: 16,
|
||||
zIndex: 9999,
|
||||
width: collapsed ? 'auto' : 420,
|
||||
maxWidth: 'calc(100vw - 280px)',
|
||||
maxHeight: 'calc(100vh - 32px)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
borderRadius: `${c.radius.xl}px`,
|
||||
bgcolor: c.bg.surface,
|
||||
border: `1px solid ${headerColor}40`,
|
||||
boxShadow: `0 8px 32px rgba(0,0,0,0.25), 0 0 0 1px ${headerColor}20`,
|
||||
overflow: 'hidden',
|
||||
animation: 'approvalSlideIn 0.25s ease-out',
|
||||
'@keyframes approvalSlideIn': {
|
||||
from: { opacity: 0, transform: 'translateY(-12px) scale(0.97)' },
|
||||
to: { opacity: 1, transform: 'translateY(0) scale(1)' },
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<Box
|
||||
onClick={() => setCollapsed((v) => !v)}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
px: 2,
|
||||
py: 1.25,
|
||||
bgcolor: headerBg,
|
||||
borderBottom: collapsed ? 'none' : `1px solid ${headerColor}20`,
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
'&:hover': { bgcolor: `${headerColor}18` },
|
||||
transition: 'background-color 0.15s',
|
||||
}}
|
||||
>
|
||||
<NotificationsActiveIcon
|
||||
sx={{
|
||||
fontSize: 18,
|
||||
color: headerColor,
|
||||
animation: hasApprovals ? 'approvalBell 0.6s ease-in-out' : 'none',
|
||||
'@keyframes approvalBell': {
|
||||
'0%': { transform: 'rotate(0)' },
|
||||
'20%': { transform: 'rotate(12deg)' },
|
||||
'40%': { transform: 'rotate(-10deg)' },
|
||||
'60%': { transform: 'rotate(6deg)' },
|
||||
'80%': { transform: 'rotate(-3deg)' },
|
||||
'100%': { transform: 'rotate(0)' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Typography sx={{ fontSize: '0.85rem', fontWeight: 700, color: headerColor, flex: 1 }}>
|
||||
{headerTitle}
|
||||
</Typography>
|
||||
{totalBadge > 0 && (
|
||||
<Chip
|
||||
label={totalBadge}
|
||||
size="small"
|
||||
sx={{
|
||||
height: 22,
|
||||
minWidth: 28,
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 700,
|
||||
bgcolor: `${headerColor}20`,
|
||||
color: headerColor,
|
||||
border: 'none',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<IconButton size="small" sx={{ color: c.text.ghost, p: 0.25 }}>
|
||||
{collapsed ? <ExpandMoreIcon sx={{ fontSize: 18 }} /> : <ExpandLessIcon sx={{ fontSize: 18 }} />}
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
{/* Content */}
|
||||
{!collapsed && (
|
||||
<Box
|
||||
sx={{
|
||||
overflow: 'auto',
|
||||
maxHeight: 'calc(100vh - 120px)',
|
||||
'&::-webkit-scrollbar': { width: 5 },
|
||||
'&::-webkit-scrollbar-track': { background: 'transparent' },
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: c.border.medium,
|
||||
borderRadius: 3,
|
||||
'&:hover': { background: c.border.strong },
|
||||
},
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: `${c.border.medium} transparent`,
|
||||
}}
|
||||
>
|
||||
{/* Approvals section */}
|
||||
{hasApprovals && (
|
||||
<Box sx={{ py: 1 }}>
|
||||
{hasAgents && (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.65rem',
|
||||
fontWeight: 700,
|
||||
color: c.text.ghost,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.06em',
|
||||
px: 2,
|
||||
pb: 0.5,
|
||||
}}
|
||||
>
|
||||
Approvals
|
||||
</Typography>
|
||||
)}
|
||||
{groups.map((group) => (
|
||||
<Box key={group.sessionId} sx={{ mb: 1, '&:last-child': { mb: 0 } }}>
|
||||
{groups.length > 1 && (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.7rem',
|
||||
fontWeight: 600,
|
||||
color: c.text.muted,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.04em',
|
||||
px: 2,
|
||||
py: 0.5,
|
||||
}}
|
||||
>
|
||||
{group.sessionName}
|
||||
</Typography>
|
||||
)}
|
||||
{group.approvals.length > 1 ? (
|
||||
<BatchApprovalBar
|
||||
requests={group.approvals}
|
||||
onApprove={onApprove}
|
||||
onDeny={onDeny}
|
||||
/>
|
||||
) : (
|
||||
group.approvals.map((req) => (
|
||||
<ApprovalBar
|
||||
key={req.id}
|
||||
request={req}
|
||||
onApprove={onApprove}
|
||||
onDeny={onDeny}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Divider between sections */}
|
||||
{hasApprovals && hasAgents && (
|
||||
<Box sx={{ mx: 2, borderTop: `1px solid ${c.border.light}` }} />
|
||||
)}
|
||||
|
||||
{/* Agent status section */}
|
||||
{hasAgents && (
|
||||
<Box sx={{ py: 1 }}>
|
||||
{hasApprovals && (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.65rem',
|
||||
fontWeight: 700,
|
||||
color: c.text.ghost,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.06em',
|
||||
px: 2,
|
||||
pb: 0.5,
|
||||
pt: 0.5,
|
||||
}}
|
||||
>
|
||||
Agents
|
||||
</Typography>
|
||||
)}
|
||||
{activeAgents.map((agent) => (
|
||||
<AgentStatusRow
|
||||
key={agent.id}
|
||||
agent={agent}
|
||||
c={c}
|
||||
onStop={onStopAgent}
|
||||
onDismiss={onDismissAgent}
|
||||
onNavigate={onNavigateToDashboard}
|
||||
/>
|
||||
))}
|
||||
{finishedAgents.map((agent) => (
|
||||
<AgentStatusRow
|
||||
key={agent.id}
|
||||
agent={agent}
|
||||
c={c}
|
||||
onStop={onStopAgent}
|
||||
onDismiss={onDismissAgent}
|
||||
onNavigate={onNavigateToDashboard}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalApprovalOverlay;
|
||||
@@ -31,7 +31,7 @@ import SystemUpdateAltIcon from '@mui/icons-material/SystemUpdateAlt';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import Settings from '@/app/pages/Settings/Settings';
|
||||
import GlobalApprovalOverlay from '@/app/components/GlobalApprovalOverlay';
|
||||
import DynamicIsland from '@/app/components/DynamicIsland';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { fetchDashboards, createDashboard, renameDashboard } from '@/shared/state/dashboardsSlice';
|
||||
import { addBrowserCard, addBrowserTab } from '@/shared/state/dashboardLayoutSlice';
|
||||
@@ -296,6 +296,8 @@ const AppShell: React.FC = () => {
|
||||
borderBottom: `0.5px solid ${c.border.medium}`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
overflow: 'visible',
|
||||
WebkitAppRegion: 'drag',
|
||||
userSelect: 'none',
|
||||
pl: '78px',
|
||||
@@ -348,35 +350,7 @@ const AppShell: React.FC = () => {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Box sx={{ flex: 1 }} />
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.75,
|
||||
pr: 1.5,
|
||||
WebkitAppRegion: 'no-drag',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="img"
|
||||
src="./logo.png"
|
||||
alt="OpenSwarm"
|
||||
sx={{ width: 18, height: 18, borderRadius: 0.5, opacity: 0.7 }}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
color: c.text.tertiary,
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 500,
|
||||
letterSpacing: 0.3,
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
OpenSwarm
|
||||
</Typography>
|
||||
</Box>
|
||||
<DynamicIsland />
|
||||
</Box>
|
||||
|
||||
{showUpdateBanner && (
|
||||
@@ -924,7 +898,6 @@ const AppShell: React.FC = () => {
|
||||
</Box>
|
||||
|
||||
<Settings />
|
||||
<GlobalApprovalOverlay />
|
||||
|
||||
<Snackbar
|
||||
open={showUpdateSnackbar}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user