[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:
haikdc
2026-03-18 04:09:39 -07:00
parent f8c2fc38b8
commit 78552982d8
8 changed files with 206 additions and 28 deletions
+1
View File
@@ -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
+2
View File
@@ -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 []),
+168 -23
View File
@@ -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) => {
+16 -1
View File
@@ -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,