From e10560416ead2b40155a2a32e30eac539df98c93 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Mon, 3 Aug 2026 16:33:51 -0700 Subject: [PATCH] [eric] shell: the dashboards picker page dies, / auto-enters the latest dashboard and self-heals when the backend answers --- frontend/src/app/Main.tsx | 4 +- .../DashboardAutoEnter/DashboardAutoEnter.tsx | 405 ++---------------- 2 files changed, 34 insertions(+), 375 deletions(-) diff --git a/frontend/src/app/Main.tsx b/frontend/src/app/Main.tsx index 74910bfc..0c274dbf 100644 --- a/frontend/src/app/Main.tsx +++ b/frontend/src/app/Main.tsx @@ -23,7 +23,7 @@ import { } from '@/shared/state/updateSlice'; import AppShell from './components/Layout/AppShell'; import ImportEntryPoint from './components/share/ImportEntryPoint'; -import DashboardSelection from './pages/DashboardSelection/DashboardSelection'; +import DashboardAutoEnter from './pages/DashboardAutoEnter/DashboardAutoEnter'; import ErrorBoundary from './components/feedback/ErrorBoundary'; import { setPanelMode, disableOnboardingAfterCrash } from '@/shared/state/onboardingProgressSlice'; @@ -530,7 +530,7 @@ const ThemedApp: React.FC = () => { }> - } /> + } /> {/* Dashboard renders persistently in AppShell so webviews survive nav. */} } /> diff --git a/frontend/src/app/pages/DashboardAutoEnter/DashboardAutoEnter.tsx b/frontend/src/app/pages/DashboardAutoEnter/DashboardAutoEnter.tsx index 1fd61b8d..abfe9abc 100644 --- a/frontend/src/app/pages/DashboardAutoEnter/DashboardAutoEnter.tsx +++ b/frontend/src/app/pages/DashboardAutoEnter/DashboardAutoEnter.tsx @@ -1,386 +1,45 @@ -import React, { useEffect, useState, useMemo } from 'react'; +import React, { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import Box from '@mui/material/Box'; -import Typography from '@mui/material/Typography'; -import Button from '@mui/material/Button'; -import TextField from '@mui/material/TextField'; -import IconButton from '@mui/material/IconButton'; -import Tooltip from '@mui/material/Tooltip'; -import Menu from '@mui/material/Menu'; -import MenuItem from '@mui/material/MenuItem'; -import ListItemIcon from '@mui/material/ListItemIcon'; -import ListItemText from '@mui/material/ListItemText'; -import AddIcon from '@mui/icons-material/Add'; -import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; -import { Skeleton } from '@/app/components/feedback/Loading'; -import ContentCopyIcon from '@mui/icons-material/ContentCopy'; -import EditIcon from '@mui/icons-material/Edit'; -import MoreVertIcon from '@mui/icons-material/MoreVert'; -import SearchIcon from '@mui/icons-material/Search'; -import { useAppDispatch, useAppSelector } from '@/shared/hooks'; -import { - fetchDashboards, - createDashboard, - deleteDashboard, - duplicateDashboard, - renameDashboard, - Dashboard, -} from '@/shared/state/dashboardsSlice'; -import { useClaudeTokens } from '@/shared/styles/ThemeContext'; +import { useAppDispatch } from '@/shared/hooks'; +import { fetchDashboards, createDashboard, Dashboard } from '@/shared/state/dashboardsSlice'; import { byPreviewRecency } from '@/shared/previewOrder'; -// Module-scope: auto-enter fires once per app boot; revisiting "/" later shows the picker normally. -let bootAutoEntered = false; - -function formatRelativeTime(dateStr: string | null): string { - if (!dateStr) return ''; - const seconds = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1000); - if (seconds < 60) return 'just now'; - const minutes = Math.floor(seconds / 60); - if (minutes < 60) return `${minutes}m ago`; - const hours = Math.floor(minutes / 60); - if (hours < 24) return `${hours}h ago`; - const days = Math.floor(hours / 24); - return `${days}d ago`; -} - -const DashboardSelection: React.FC = () => { - const c = useClaudeTokens(); +// The old picker page is gone: the Spaces strip owns dashboard switching, so landing on "/" just +// drops the user into their latest dashboard (creating the first one on a fresh install). Renders +// nothing; while the backend is down the shell's warning banner is the message, and the retry loop +// self-heals the moment it answers. +const DashboardAutoEnter: React.FC = () => { const dispatch = useAppDispatch(); const navigate = useNavigate(); - const items = useAppSelector((state) => state.dashboards.items); - const loading = useAppSelector((state) => state.dashboards.loading); - - const [search, setSearch] = useState(''); - const [menuAnchor, setMenuAnchor] = useState(null); - const [menuPosition, setMenuPosition] = useState<{ top: number; left: number } | null>(null); - const [menuDashboard, setMenuDashboard] = useState(null); - const [renamingId, setRenamingId] = useState(null); - const [renameValue, setRenameValue] = useState(''); useEffect(() => { - if (bootAutoEntered) { - dispatch(fetchDashboards()); - return; - } - // First mount of the session: skip the picker and drop the user into their latest dashboard. - bootAutoEntered = true; - (async () => { + let cancelled = false; + let timer: ReturnType | null = null; + const enter = async (): Promise => { const res = await dispatch(fetchDashboards()); - if (!fetchDashboards.fulfilled.match(res)) return; - const list = (res.payload as Dashboard[]).slice().sort(byPreviewRecency); - if (list.length > 0) { - navigate(`/dashboard/${list[0].id}`, { replace: true }); - return; + if (cancelled) return; + if (fetchDashboards.fulfilled.match(res)) { + const list = (res.payload as Dashboard[]).slice().sort(byPreviewRecency); + if (list.length > 0) { + navigate(`/dashboard/${list[0].id}`, { replace: true }); + return; + } + const created = await dispatch(createDashboard('Untitled Dashboard')); + if (!cancelled && createDashboard.fulfilled.match(created)) { + navigate(`/dashboard/${created.payload.id}`, { replace: true }); + return; + } } - const created = await dispatch(createDashboard('Untitled Dashboard')); - if (createDashboard.fulfilled.match(created)) { - navigate(`/dashboard/${created.payload.id}`, { replace: true }); - } - })(); + if (!cancelled) timer = setTimeout(enter, 3000); + }; + enter(); + return () => { + cancelled = true; + if (timer) clearTimeout(timer); + }; }, [dispatch, navigate]); - const dashboards = useMemo(() => { - const all = Object.values(items).sort(byPreviewRecency); - if (!search.trim()) return all; - const q = search.toLowerCase(); - return all.filter((d) => d.name.toLowerCase().includes(q)); - }, [items, search]); - - const handleCreate = async () => { - const result = await dispatch(createDashboard('Untitled Dashboard')); - if (createDashboard.fulfilled.match(result)) { - navigate(`/dashboard/${result.payload.id}`); - } - }; - - const handleOpenMenu = (e: React.MouseEvent, d: Dashboard) => { - e.stopPropagation(); - setMenuAnchor(e.currentTarget); - setMenuDashboard(d); - }; - - // Right-click anywhere on a card opens the same menu at the cursor, Mac-style. - const handleContextMenu = (e: React.MouseEvent, d: Dashboard) => { - e.preventDefault(); - e.stopPropagation(); - setMenuPosition({ top: e.clientY, left: e.clientX }); - setMenuDashboard(d); - }; - - const handleCloseMenu = () => { - setMenuAnchor(null); - setMenuPosition(null); - setMenuDashboard(null); - }; - - const handleDelete = () => { - if (menuDashboard) dispatch(deleteDashboard(menuDashboard.id)); - handleCloseMenu(); - }; - - const handleDuplicate = () => { - if (menuDashboard) dispatch(duplicateDashboard(menuDashboard.id)); - handleCloseMenu(); - }; - - const handleStartRename = () => { - const target = menuDashboard; - handleCloseMenu(); - if (target) { - setTimeout(() => { - setRenamingId(target.id); - setRenameValue(target.name); - }, 150); - } - }; - - const handleRenameSubmit = (id: string) => { - const trimmed = renameValue.trim(); - const previousName = items[id]?.name; - if (trimmed && trimmed !== previousName) { - dispatch(renameDashboard({ id, name: trimmed, previousName })); - } - setRenamingId(null); - }; - - return ( - - - - - - Dashboards - - - Monitor and manage your agents from a single workspace. - - - - - - - setSearch(e.target.value)} - InputProps={{ - startAdornment: ( - - ), - }} - sx={{ - width: 320, - '& .MuiOutlinedInput-root': { - bgcolor: c.bg.surface, - borderRadius: 2, - fontSize: '0.875rem', - '& fieldset': { borderColor: c.border.subtle }, - '&:hover fieldset': { borderColor: c.border.medium }, - }, - }} - /> - - - {loading ? ( - - {[0, 1, 2].map((i) => ( - - ))} - - ) : dashboards.length === 0 ? ( - - - {search ? 'No dashboards match your search' : 'No dashboards yet'} - - - {search ? 'Try a different search term' : 'Create your first dashboard to get started'} - - - ) : ( - - {dashboards.map((d) => ( - { - if (renamingId === d.id) return; - navigate(`/dashboard/${d.id}`); - }} - onContextMenu={(e) => handleContextMenu(e, d)} - sx={{ - cursor: renamingId === d.id ? 'default' : 'pointer', - borderRadius: 3, - border: `1px solid ${c.border.subtle}`, - bgcolor: c.bg.surface, - overflow: 'hidden', - transition: 'all 0.2s ease', - // One elevation cue: the shadow fades in on hover, the card doesn't jump. - '&:hover': { - borderColor: c.border.strong, - boxShadow: c.shadow.md, - }, - '&:hover .card-actions': { opacity: 1 }, - display: 'flex', - flexDirection: 'column', - }} - > - - {d.thumbnail ? ( - - ) : null} - - - handleOpenMenu(e, d)} - sx={{ - bgcolor: c.bg.surface, - color: c.text.muted, - boxShadow: c.shadow.sm, - '&:hover': { bgcolor: c.bg.elevated }, - }} - > - - - - - - - - {renamingId === d.id ? ( - setRenameValue(e.target.value)} - onBlur={() => handleRenameSubmit(d.id)} - onKeyDown={(e) => { - if (e.key === 'Enter') handleRenameSubmit(d.id); - if (e.key === 'Escape') setRenamingId(null); - }} - onClick={(e) => e.stopPropagation()} - sx={{ - '& .MuiOutlinedInput-root': { - fontSize: '1rem', - fontWeight: 600, - }, - }} - /> - ) : ( - - {d.name} - - )} - - Updated {formatRelativeTime(d.updated_at)} - - - - ))} - - )} - - - - - - Rename - - - - Duplicate - - - - Delete - - - - ); + return null; }; -export default DashboardSelection; +export default DashboardAutoEnter;