From 78552982d83b2c0a1831b46d44883da2239bb14c Mon Sep 17 00:00:00 2001 From: haikdc Date: Wed, 18 Mar 2026 04:09:39 -0700 Subject: [PATCH] [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) --- backend/apps/settings/models.py | 1 + backend/apps/tools_lib/tools_lib.py | 2 + .../src/app/components/Layout/AppShell.tsx | 191 +++++++++++++++--- .../app/components/useDomElementSelector.ts | 15 +- .../src/app/pages/AgentChat/ChatInput.tsx | 1 + .../src/app/pages/Dashboard/Dashboard.tsx | 5 +- frontend/src/app/pages/Settings/Settings.tsx | 17 +- frontend/src/shared/state/settingsSlice.ts | 2 + 8 files changed, 206 insertions(+), 28 deletions(-) diff --git a/backend/apps/settings/models.py b/backend/apps/settings/models.py index f2ef9988..14d633d4 100644 --- a/backend/apps/settings/models.py +++ b/backend/apps/settings/models.py @@ -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 diff --git a/backend/apps/tools_lib/tools_lib.py b/backend/apps/tools_lib/tools_lib.py index 274f077c..2d7c8bba 100644 --- a/backend/apps/tools_lib/tools_lib.py +++ b/backend/apps/tools_lib/tools_lib.py @@ -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 []), diff --git a/frontend/src/app/components/Layout/AppShell.tsx b/frontend/src/app/components/Layout/AppShell.tsx index 2d4edc02..2e1ccb79 100644 --- a/frontend/src/app/components/Layout/AppShell.tsx +++ b/frontend/src/app/components/Layout/AppShell.tsx @@ -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: }, @@ -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(() => { + 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 = () => { + {showUpdateBanner && ( + + + + {updateStatus === 'available' && `OpenSwarm ${availableVersion} is available`} + {updateStatus === 'downloading' && `Downloading OpenSwarm ${availableVersion}…`} + {updateStatus === 'downloaded' && `OpenSwarm ${availableVersion} is ready to install`} + + {updateStatus === 'downloading' && ( + + )} + {updateStatus === 'downloading' && ( + + {Math.round(downloadPercent)}% + + )} + {updateStatus === 'available' && ( + + )} + {updateStatus === 'downloaded' && ( + + )} + + + + + )} + {!sidebarCollapsed && ( <> @@ -735,36 +855,60 @@ const AppShell: React.FC = () => { setSnackbarDismissed(true)} anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} > } + icon={updateStatus === 'downloaded' + ? + : + } action={ - + {updateStatus === 'available' && ( + + )} + {updateStatus === 'downloaded' && ( + + )} } 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`} diff --git a/frontend/src/app/components/useDomElementSelector.ts b/frontend/src/app/components/useDomElementSelector.ts index b4535e19..8115769b 100644 --- a/frontend/src/app/components/useDomElementSelector.ts +++ b/frontend/src/app/components/useDomElementSelector.ts @@ -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(null); const excludeIdRef = useRef(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]); diff --git a/frontend/src/app/pages/AgentChat/ChatInput.tsx b/frontend/src/app/pages/AgentChat/ChatInput.tsx index ff028b5d..525bd254 100644 --- a/frontend/src/app/pages/AgentChat/ChatInput.tsx +++ b/frontend/src/app/pages/AgentChat/ChatInput.tsx @@ -981,6 +981,7 @@ const ChatInput = forwardRef(({ onSend, disabled, mode, e.preventDefault()} onClick={() => { if (!elementSelection.selectMode && sessionId) { elementSelection.setExcludeSelectId(sessionId); diff --git a/frontend/src/app/pages/Dashboard/Dashboard.tsx b/frontend/src/app/pages/Dashboard/Dashboard.tsx index 79c62974..9d597b05 100644 --- a/frontend/src/app/pages/Dashboard/Dashboard.tsx +++ b/frontend/src/app/pages/Dashboard/Dashboard.tsx @@ -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) => { diff --git a/frontend/src/app/pages/Settings/Settings.tsx b/frontend/src/app/pages/Settings/Settings.tsx index 58d71b10..bae49d38 100644 --- a/frontend/src/app/pages/Settings/Settings.tsx +++ b/frontend/src/app/pages/Settings/Settings.tsx @@ -554,7 +554,7 @@ const Settings: React.FC = () => { - + Auto-enable element selection Automatically enter element selection mode when creating a new agent. @@ -569,6 +569,21 @@ const Settings: React.FC = () => { /> + + + Default agent spawn state in dashboard + When enabled, new agents spawn expanded instead of collapsed. + + 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 }, + }} + /> + + {/* ── Browser ── */} Browser diff --git a/frontend/src/shared/state/settingsSlice.ts b/frontend/src/shared/state/settingsSlice.ts index 4530871c..0d7acce8 100644 --- a/frontend/src/shared/state/settingsSlice.ts +++ b/frontend/src/shared/state/settingsSlice.ts @@ -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,