mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-28 04:24:51 +02:00
[Haik]: ckpt (input focus persistance in select mode) (can set default spawn in state of a card from expanded to collapsed in settings) (Settings update notification is more in ur face now - to be tested in prod)
This commit is contained in:
@@ -22,3 +22,4 @@ class AppSettings(BaseModel):
|
||||
anthropic_api_key: Optional[str] = None
|
||||
browser_homepage: str = "https://www.google.com"
|
||||
auto_select_mode_on_new_agent: bool = False
|
||||
expand_new_chats_in_dashboard: bool = False
|
||||
|
||||
@@ -382,6 +382,7 @@ def derive_mcp_config(tool: ToolDefinition) -> Optional[dict]:
|
||||
config["command"] = resolved
|
||||
env = config.setdefault("env", {})
|
||||
env.setdefault("PATH", _augmented_path())
|
||||
env.setdefault("PYTHONPATH", "")
|
||||
|
||||
return config
|
||||
|
||||
@@ -548,6 +549,7 @@ async def _discover_mcp_tools_stdio(command: str, args: list[str] | None = None,
|
||||
raise HTTPException(status_code=400, detail=f"Command '{command}' not found on PATH or common install locations")
|
||||
|
||||
proc_env = {**os.environ, **(env or {}), "PATH": _augmented_path()}
|
||||
proc_env.pop("PYTHONPATH", None)
|
||||
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
cmd_path, *(args or []),
|
||||
|
||||
@@ -27,6 +27,9 @@ import ViewSidebarOutlinedIcon from '@mui/icons-material/ViewSidebarOutlined';
|
||||
import ArrowBackOutlinedIcon from '@mui/icons-material/ArrowBackOutlined';
|
||||
import ArrowForwardOutlinedIcon from '@mui/icons-material/ArrowForwardOutlined';
|
||||
import RestartAltIcon from '@mui/icons-material/RestartAlt';
|
||||
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 { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
@@ -38,6 +41,7 @@ const SIDEBAR_MIN = 160;
|
||||
const SIDEBAR_MAX = 400;
|
||||
const SIDEBAR_DEFAULT = 220;
|
||||
const SIDEBAR_WIDTH_KEY = 'openswarm-sidebar-width';
|
||||
const UPDATE_DISMISS_KEY = 'openswarm-update-dismissed';
|
||||
|
||||
const CUSTOMIZATION_ITEMS = [
|
||||
{ label: 'Prompts', path: '/templates', icon: <DescriptionIcon /> },
|
||||
@@ -75,10 +79,34 @@ const AppShell: React.FC = () => {
|
||||
|
||||
const updateStatus = useAppSelector((state) => state.update.status);
|
||||
const availableVersion = useAppSelector((state) => state.update.availableVersion);
|
||||
const [updateBannerDismissed, setUpdateBannerDismissed] = useState(false);
|
||||
const downloadPercent = useAppSelector((state) => state.update.downloadPercent);
|
||||
|
||||
const showUpdateDot = updateStatus === 'available' || updateStatus === 'downloaded';
|
||||
const showUpdateBanner = updateStatus === 'downloaded' && !updateBannerDismissed;
|
||||
const [dismissedVersion, setDismissedVersion] = useState<string | null>(() => {
|
||||
try { return localStorage.getItem(UPDATE_DISMISS_KEY); } catch { return null; }
|
||||
});
|
||||
const [snackbarDismissed, setSnackbarDismissed] = useState(false);
|
||||
|
||||
const bannerDismissedForVersion = availableVersion != null && dismissedVersion === availableVersion;
|
||||
const isUpdateActionable = updateStatus === 'available' || updateStatus === 'downloaded' || updateStatus === 'downloading';
|
||||
|
||||
const showUpdateDot = (updateStatus === 'available' || updateStatus === 'downloaded') && !bannerDismissedForVersion;
|
||||
const showUpdateBanner = isUpdateActionable && !bannerDismissedForVersion;
|
||||
const showUpdateSnackbar = (updateStatus === 'available' || updateStatus === 'downloaded') && !bannerDismissedForVersion && !snackbarDismissed;
|
||||
|
||||
const handleDismissBanner = useCallback(() => {
|
||||
if (availableVersion) {
|
||||
try { localStorage.setItem(UPDATE_DISMISS_KEY, availableVersion); } catch {}
|
||||
setDismissedVersion(availableVersion);
|
||||
}
|
||||
}, [availableVersion]);
|
||||
|
||||
const handleDownloadUpdate = useCallback(async () => {
|
||||
try { await (window as any).openswarm?.downloadUpdate(); } catch {}
|
||||
}, []);
|
||||
|
||||
const handleInstallUpdate = useCallback(() => {
|
||||
(window as any).openswarm?.installUpdate();
|
||||
}, []);
|
||||
|
||||
const dashboardItems = useAppSelector((state) => state.dashboards.items);
|
||||
const dashboardList = Object.values(dashboardItems).sort(
|
||||
@@ -279,6 +307,98 @@ const AppShell: React.FC = () => {
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{showUpdateBanner && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1.5,
|
||||
px: 2,
|
||||
py: 0.5,
|
||||
bgcolor: `${c.accent.primary}14`,
|
||||
borderBottom: `1px solid ${c.accent.primary}30`,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<SystemUpdateAltIcon sx={{ fontSize: 16, color: c.accent.primary, flexShrink: 0 }} />
|
||||
<Typography sx={{ fontSize: '0.8rem', color: c.text.secondary, flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{updateStatus === 'available' && `OpenSwarm ${availableVersion} is available`}
|
||||
{updateStatus === 'downloading' && `Downloading OpenSwarm ${availableVersion}…`}
|
||||
{updateStatus === 'downloaded' && `OpenSwarm ${availableVersion} is ready to install`}
|
||||
</Typography>
|
||||
{updateStatus === 'downloading' && (
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={downloadPercent}
|
||||
sx={{
|
||||
width: 120,
|
||||
height: 3,
|
||||
flexShrink: 0,
|
||||
borderRadius: 2,
|
||||
bgcolor: `${c.accent.primary}20`,
|
||||
'& .MuiLinearProgress-bar': { bgcolor: c.accent.primary, borderRadius: 2 },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{updateStatus === 'downloading' && (
|
||||
<Typography sx={{ fontSize: '0.72rem', color: c.text.tertiary, flexShrink: 0 }}>
|
||||
{Math.round(downloadPercent)}%
|
||||
</Typography>
|
||||
)}
|
||||
{updateStatus === 'available' && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={handleDownloadUpdate}
|
||||
sx={{
|
||||
bgcolor: c.accent.primary,
|
||||
'&:hover': { bgcolor: c.accent.pressed },
|
||||
textTransform: 'none',
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 600,
|
||||
borderRadius: 1.5,
|
||||
minWidth: 'auto',
|
||||
py: 0.25,
|
||||
px: 1.5,
|
||||
lineHeight: 1.5,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
)}
|
||||
{updateStatus === 'downloaded' && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={handleInstallUpdate}
|
||||
sx={{
|
||||
bgcolor: c.accent.primary,
|
||||
'&:hover': { bgcolor: c.accent.pressed },
|
||||
textTransform: 'none',
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 600,
|
||||
borderRadius: 1.5,
|
||||
minWidth: 'auto',
|
||||
py: 0.25,
|
||||
px: 1.5,
|
||||
lineHeight: 1.5,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
Restart & Update
|
||||
</Button>
|
||||
)}
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleDismissBanner}
|
||||
sx={{ color: c.text.tertiary, p: 0.25, flexShrink: 0, '&:hover': { color: c.text.secondary } }}
|
||||
>
|
||||
<CloseIcon sx={{ fontSize: 14 }} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box sx={{ display: 'flex', flex: 1, minHeight: 0 }}>
|
||||
{!sidebarCollapsed && (
|
||||
<>
|
||||
@@ -735,36 +855,60 @@ const AppShell: React.FC = () => {
|
||||
<GlobalApprovalOverlay />
|
||||
|
||||
<Snackbar
|
||||
open={showUpdateBanner}
|
||||
open={showUpdateSnackbar}
|
||||
autoHideDuration={10000}
|
||||
onClose={() => setSnackbarDismissed(true)}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
>
|
||||
<Alert
|
||||
severity="info"
|
||||
icon={<RestartAltIcon sx={{ fontSize: 18 }} />}
|
||||
icon={updateStatus === 'downloaded'
|
||||
? <RestartAltIcon sx={{ fontSize: 18 }} />
|
||||
: <SystemUpdateAltIcon sx={{ fontSize: 18 }} />
|
||||
}
|
||||
action={
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => setUpdateBannerDismissed(true)}
|
||||
onClick={() => setSnackbarDismissed(true)}
|
||||
sx={{ color: c.text.muted, textTransform: 'none', fontSize: '0.8rem', minWidth: 'auto' }}
|
||||
>
|
||||
Later
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={() => (window as any).openswarm?.installUpdate()}
|
||||
sx={{
|
||||
bgcolor: c.accent.primary,
|
||||
'&:hover': { bgcolor: c.accent.pressed },
|
||||
textTransform: 'none',
|
||||
fontSize: '0.8rem',
|
||||
borderRadius: 1.5,
|
||||
minWidth: 'auto',
|
||||
}}
|
||||
>
|
||||
Restart
|
||||
Dismiss
|
||||
</Button>
|
||||
{updateStatus === 'available' && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={handleDownloadUpdate}
|
||||
sx={{
|
||||
bgcolor: c.accent.primary,
|
||||
'&:hover': { bgcolor: c.accent.pressed },
|
||||
textTransform: 'none',
|
||||
fontSize: '0.8rem',
|
||||
borderRadius: 1.5,
|
||||
minWidth: 'auto',
|
||||
}}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
)}
|
||||
{updateStatus === 'downloaded' && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={handleInstallUpdate}
|
||||
sx={{
|
||||
bgcolor: c.accent.primary,
|
||||
'&:hover': { bgcolor: c.accent.pressed },
|
||||
textTransform: 'none',
|
||||
fontSize: '0.8rem',
|
||||
borderRadius: 1.5,
|
||||
minWidth: 'auto',
|
||||
}}
|
||||
>
|
||||
Restart & Update
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
sx={{
|
||||
@@ -775,7 +919,8 @@ const AppShell: React.FC = () => {
|
||||
'& .MuiAlert-icon': { color: c.accent.primary },
|
||||
}}
|
||||
>
|
||||
OpenSwarm {availableVersion} downloaded — restart to update
|
||||
{updateStatus === 'available' && `OpenSwarm ${availableVersion} is available`}
|
||||
{updateStatus === 'downloaded' && `OpenSwarm ${availableVersion} downloaded — restart to update`}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</Box>
|
||||
|
||||
@@ -120,6 +120,7 @@ export function useDomElementSelector(): DomSelectorState {
|
||||
const dragOriginRef = useRef<{ x: number; y: number } | null>(null);
|
||||
const isDraggingRef = useRef(false);
|
||||
const dragBoundsRef = useRef<{ left: number; top: number; right: number; bottom: number } | null>(null);
|
||||
const preDragFocusRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
const excludeIdRef = useRef<string | null>(null);
|
||||
useEffect(() => {
|
||||
@@ -245,8 +246,11 @@ export function useDomElementSelector(): DomSelectorState {
|
||||
if (e.button !== 0) return;
|
||||
if (e.metaKey || e.ctrlKey) return;
|
||||
const target = e.target as Element;
|
||||
// Only start drag on "empty" canvas areas (not on selectable elements)
|
||||
if (target && findSelectableAncestor(target, excludeIdRef.current)) return;
|
||||
if (target && findSelectableAncestor(target, excludeIdRef.current)) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
preDragFocusRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
||||
dragOriginRef.current = { x: e.clientX, y: e.clientY };
|
||||
isDraggingRef.current = false;
|
||||
}, []);
|
||||
@@ -291,12 +295,17 @@ export function useDomElementSelector(): DomSelectorState {
|
||||
});
|
||||
}
|
||||
|
||||
const wasDragging = isDraggingRef.current;
|
||||
dragOriginRef.current = null;
|
||||
isDraggingRef.current = false;
|
||||
dragBoundsRef.current = null;
|
||||
setDragRect(EMPTY_DRAG);
|
||||
setDragPreview([]);
|
||||
if (dragPreviewRafRef.current) cancelAnimationFrame(dragPreviewRafRef.current);
|
||||
if (wasDragging && preDragFocusRef.current) {
|
||||
preDragFocusRef.current.focus();
|
||||
}
|
||||
preDragFocusRef.current = null;
|
||||
}, [ctx]);
|
||||
|
||||
const handleClick = useCallback((e: MouseEvent) => {
|
||||
@@ -327,6 +336,7 @@ export function useDomElementSelector(): DomSelectorState {
|
||||
dragOriginRef.current = null;
|
||||
dragBoundsRef.current = null;
|
||||
isDraggingRef.current = false;
|
||||
preDragFocusRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -353,6 +363,7 @@ export function useDomElementSelector(): DomSelectorState {
|
||||
dragOriginRef.current = null;
|
||||
dragBoundsRef.current = null;
|
||||
isDraggingRef.current = false;
|
||||
preDragFocusRef.current = null;
|
||||
};
|
||||
}, [ctx?.selectMode, handleMouseMove, handleMouseDown, handleMouseUp, handleClick]);
|
||||
|
||||
|
||||
@@ -981,6 +981,7 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(({ onSend, disabled, mode,
|
||||
<Tooltip title={elementSelection.selectMode ? 'Exit select mode' : 'Select UI element'}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => {
|
||||
if (!elementSelection.selectMode && sessionId) {
|
||||
elementSelection.setExcludeSelectId(sessionId);
|
||||
|
||||
@@ -84,6 +84,7 @@ const DashboardInner: React.FC = () => {
|
||||
const zoomSensitivity = useAppSelector((state) => state.settings.data.zoom_sensitivity);
|
||||
const newAgentShortcut = useAppSelector((state) => state.settings.data.new_agent_shortcut);
|
||||
const browserHomepage = useAppSelector((state) => state.settings.data.browser_homepage);
|
||||
const expandNewChats = useAppSelector((state) => state.settings.data.expand_new_chats_in_dashboard);
|
||||
const outputs = useAppSelector((state) => state.outputs.items);
|
||||
const sessionList = Object.values(sessions);
|
||||
|
||||
@@ -420,7 +421,7 @@ const DashboardInner: React.FC = () => {
|
||||
contextPaths: contextPaths?.map((cp) => ({ path: cp.path, type: cp.type })),
|
||||
forcedTools,
|
||||
attachedSkills,
|
||||
expand: false,
|
||||
expand: expandNewChats,
|
||||
}),
|
||||
).then((action) => {
|
||||
if (launchAndSendFirstMessage.fulfilled.match(action)) {
|
||||
@@ -452,7 +453,7 @@ const DashboardInner: React.FC = () => {
|
||||
}
|
||||
});
|
||||
},
|
||||
[canvas.viewportRef, dispatch, dashboardId],
|
||||
[canvas.viewportRef, dispatch, dashboardId, expandNewChats],
|
||||
);
|
||||
|
||||
const handleAddView = useCallback((outputId: string) => {
|
||||
|
||||
@@ -554,7 +554,7 @@ const Settings: React.FC = () => {
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowLastSx}>
|
||||
<Box sx={inlineRowSx}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>Auto-enable element selection</Typography>
|
||||
<Typography sx={descSx}>Automatically enter element selection mode when creating a new agent.</Typography>
|
||||
@@ -569,6 +569,21 @@ const Settings: React.FC = () => {
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={inlineRowLastSx}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>Default agent spawn state in dashboard</Typography>
|
||||
<Typography sx={descSx}>When enabled, new agents spawn expanded instead of collapsed.</Typography>
|
||||
</Box>
|
||||
<Switch
|
||||
checked={form.expand_new_chats_in_dashboard}
|
||||
onChange={(e) => setForm({ ...form, expand_new_chats_in_dashboard: e.target.checked })}
|
||||
sx={{
|
||||
'& .MuiSwitch-switchBase.Mui-checked': { color: c.accent.primary },
|
||||
'& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { bgcolor: c.accent.primary },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* ── Browser ── */}
|
||||
<Typography sx={{ ...sectionSx, mt: 3 }}>Browser</Typography>
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface AppSettings {
|
||||
anthropic_api_key: string | null;
|
||||
browser_homepage: string;
|
||||
auto_select_mode_on_new_agent: boolean;
|
||||
expand_new_chats_in_dashboard: boolean;
|
||||
}
|
||||
|
||||
export interface BrowseResult {
|
||||
@@ -52,6 +53,7 @@ const initialState: SettingsState = {
|
||||
anthropic_api_key: null,
|
||||
browser_homepage: 'https://www.google.com',
|
||||
auto_select_mode_on_new_agent: false,
|
||||
expand_new_chats_in_dashboard: false,
|
||||
},
|
||||
loading: false,
|
||||
loaded: false,
|
||||
|
||||
Reference in New Issue
Block a user