diff --git a/frontend/src/app/components/UpdateReadyDot.tsx b/frontend/src/app/components/UpdateReadyDot.tsx
new file mode 100644
index 00000000..fea0d923
--- /dev/null
+++ b/frontend/src/app/components/UpdateReadyDot.tsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import Box from '@mui/material/Box';
+import { useAppSelector } from '@/shared/hooks';
+import { useClaudeTokens } from '@/shared/styles/ThemeContext';
+import type { SxProps } from '@mui/material/styles';
+
+/** The breadcrumb dot for a downloaded update: gear tile -> Advanced rail row -> the relaunch row. Renders nothing until the update is actually sitting on disk, so it can never nag about something a click cannot deliver. */
+const UpdateReadyDot: React.FC<{ size?: number; sx?: SxProps }> = ({ size = 8, sx }) => {
+ const c = useClaudeTokens();
+ const ready = useAppSelector((s) => s.update.status === 'downloaded' && !s.update.installing);
+ if (!ready) return null;
+ return (
+
+ );
+};
+
+export default UpdateReadyDot;
diff --git a/frontend/src/app/pages/Dashboard/desktop/DockActionTiles.tsx b/frontend/src/app/pages/Dashboard/desktop/DockActionTiles.tsx
index a2a0e73d..08129d25 100644
--- a/frontend/src/app/pages/Dashboard/desktop/DockActionTiles.tsx
+++ b/frontend/src/app/pages/Dashboard/desktop/DockActionTiles.tsx
@@ -5,6 +5,7 @@ import { Globe, CalendarClock, Settings, LayoutGrid, Store } from 'lucide-react'
import { useAppDispatch } from '@/shared/hooks';
import { openSettingsCard, openWorkflowsApp } from '@/shared/state/dashboardLayoutSlice';
import { openMarketplace } from '@/app/pages/Directory/openMarketplace';
+import UpdateReadyDot from '@/app/components/UpdateReadyDot';
// The dock reserves room for these before it knows what they are, so the count lives with the list.
export const DOCK_ACTION_COUNT = 5;
@@ -19,11 +20,11 @@ interface DockActionTilesProps {
/** The dock's fixed group: browser, workflows, then settings + applications under their own divider. Action buttons are flat hairline glyphs resting on the rail material (running dock tiles above keep their colored tiles), so actions and running things read as different species. */
function DockActionTiles({ tile, onAddBrowser, onApplications, onHoverAway }: DockActionTilesProps): React.ReactElement {
const dispatch = useAppDispatch();
- const actions: { label: string; Icon: typeof Globe; act: () => void; divider?: boolean }[] = [
+ const actions: { label: string; Icon: typeof Globe; act: () => void; divider?: boolean; updateDot?: boolean }[] = [
{ label: 'Browsers', Icon: Globe, act: onAddBrowser },
{ label: 'Workflows', Icon: CalendarClock, act: () => dispatch(openWorkflowsApp()) },
{ label: 'Marketplace', Icon: Store, act: () => openMarketplace() },
- { label: 'Settings', Icon: Settings, act: () => dispatch(openSettingsCard()), divider: true },
+ { label: 'Settings', Icon: Settings, act: () => dispatch(openSettingsCard()), divider: true, updateDot: true },
{ label: 'Applications', Icon: LayoutGrid, act: onApplications },
];
@@ -38,6 +39,7 @@ function DockActionTiles({ tile, onAddBrowser, onApplications, onHoverAway }: Do
onClick={a.act}
onMouseEnter={onHoverAway}
sx={{
+ position: 'relative',
width: tile,
height: tile,
borderRadius: '10px',
@@ -53,6 +55,7 @@ function DockActionTiles({ tile, onAddBrowser, onApplications, onHoverAway }: Do
}}
>
+ {a.updateDot && }
diff --git a/frontend/src/app/pages/Settings/sections/SettingsRail.tsx b/frontend/src/app/pages/Settings/sections/SettingsRail.tsx
index 65b41ad2..732765e6 100644
--- a/frontend/src/app/pages/Settings/sections/SettingsRail.tsx
+++ b/frontend/src/app/pages/Settings/sections/SettingsRail.tsx
@@ -4,6 +4,7 @@ import Typography from '@mui/material/Typography';
import { User, Settings2, Palette, ShieldCheck, Wrench, Boxes, SquareSlash, BarChart3, Bell, Mic, LayoutGrid, Bot, Brain } from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
+import UpdateReadyDot from '@/app/components/UpdateReadyDot';
// ChatGPT/Apple-style settings nav: a left rail of short, focused sections instead of one giant
// General scroll. Grouped so the eye lands (account, then app-wide, then capabilities).
@@ -89,6 +90,7 @@ const SettingsRail: React.FC<{
>
{s.label}
+ {s.value === 'advanced' && }
);
})}
diff --git a/frontend/src/app/pages/Settings/sections/general/GeneralAdvanced.tsx b/frontend/src/app/pages/Settings/sections/general/GeneralAdvanced.tsx
index 196c2c03..3f88b456 100644
--- a/frontend/src/app/pages/Settings/sections/general/GeneralAdvanced.tsx
+++ b/frontend/src/app/pages/Settings/sections/general/GeneralAdvanced.tsx
@@ -11,7 +11,7 @@ import { onboardingBus } from '@/app/components/Onboarding/eventBus';
import { resetTour } from '@/shared/state/onboardingProgressSlice';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import TrustedFilePatterns from '@/app/components/overlays/TrustedFilePatterns';
-import SoftwareUpdateRow from './SoftwareUpdateRow';
+import SoftwareUpdateRow, { UpdateReadyBanner } from './SoftwareUpdateRow';
import type { SettingsStyles } from '../settingsStyles';
import { settingSelectAttrs } from '../settingSelect';
@@ -36,6 +36,7 @@ const GeneralAdvanced: React.FC<{
return (
<>
+
diff --git a/frontend/src/app/pages/Settings/sections/general/SoftwareUpdateRow.tsx b/frontend/src/app/pages/Settings/sections/general/SoftwareUpdateRow.tsx
index a2ca42fb..137ef9a6 100644
--- a/frontend/src/app/pages/Settings/sections/general/SoftwareUpdateRow.tsx
+++ b/frontend/src/app/pages/Settings/sections/general/SoftwareUpdateRow.tsx
@@ -14,6 +14,49 @@ import { setChecking, setUpdateError, setInstalling } from '@/shared/state/updat
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import type { SettingsStyles } from '../settingsStyles';
+/** The breadcrumb's last stop: a pill-shaped relaunch row pinned to the top of Advanced while an update sits downloaded. */
+export const UpdateReadyBanner: React.FC = () => {
+ const c = useClaudeTokens();
+ const dispatch = useAppDispatch();
+ const updateStatus = useAppSelector((s) => s.update.status);
+ const availableVersion = useAppSelector((s) => s.update.availableVersion);
+ const installing = useAppSelector((s) => s.update.installing);
+ if (updateStatus !== 'downloaded') return null;
+ const install = () => {
+ if (installing) return;
+ dispatch(setInstalling());
+ (window as any).openswarm?.installUpdate();
+ };
+ return (
+
+
+
+
+ {installing ? 'Relaunching…' : 'Relaunch to update'}
+
+
+ {availableVersion ? `Version ${availableVersion} is downloaded and ready.` : 'The update is downloaded and ready.'}
+
+
+ {installing && }
+
+ );
+};
+
const SoftwareUpdateRow: React.FC<{ styles: SettingsStyles }> = ({ styles }) => {
const c = useClaudeTokens();
const dispatch = useAppDispatch();