From 03cec8d507e42a42af8c4ad212241be3c6340ff1 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Fri, 20 Mar 2026 02:04:33 +0200 Subject: [PATCH] fix: restore reliable update flow across sidebar and mobile Show Update button in sidebar only when an update is available Support update button on mobile sidebar and remove duplicate mobile Settings entry Force desktop Tauri recheck and preflight before download to avoid missing pending updates --- .../src/components/session/SessionSidebar.tsx | 44 +++++++++++++++ .../session/sidebar/SidebarFooter.tsx | 22 +++++++- .../ui/src/components/views/SettingsView.tsx | 6 --- packages/ui/src/stores/useUpdateStore.ts | 53 ++++++++++++++++++- 4 files changed, 116 insertions(+), 9 deletions(-) diff --git a/packages/ui/src/components/session/SessionSidebar.tsx b/packages/ui/src/components/session/SessionSidebar.tsx index d4910c70..2dde7c94 100644 --- a/packages/ui/src/components/session/SessionSidebar.tsx +++ b/packages/ui/src/components/session/SessionSidebar.tsx @@ -36,12 +36,14 @@ import { useSessionFolderCleanup } from './sidebar/hooks/useSessionFolderCleanup import { useStickyProjectHeaders } from './sidebar/hooks/useStickyProjectHeaders'; import { useGitHubPrStatusStore } from '@/stores/useGitHubPrStatusStore'; import { ProjectEditDialog } from '@/components/layout/ProjectEditDialog'; +import { UpdateDialog } from '@/components/ui/UpdateDialog'; import { SessionGroupSection } from './sidebar/SessionGroupSection'; import { SidebarHeader } from './sidebar/SidebarHeader'; import { SidebarActivitySections } from './sidebar/SidebarActivitySections'; import { SidebarFooter } from './sidebar/SidebarFooter'; import { SidebarProjectsList } from './sidebar/SidebarProjectsList'; import { SessionNodeItem } from './sidebar/SessionNodeItem'; +import { useUpdateStore } from '@/stores/useUpdateStore'; import type { SortableDragHandleProps } from './sidebar/sortableItems'; import { FolderDeleteConfirmDialog, @@ -170,6 +172,7 @@ export const SessionSidebar: React.FC = ({ const [expandedSessionGroups, setExpandedSessionGroups] = React.useState>(new Set()); const [hoveredProjectId, setHoveredProjectId] = React.useState(null); const [newWorktreeDialogOpen, setNewWorktreeDialogOpen] = React.useState(false); + const [updateDialogOpen, setUpdateDialogOpen] = React.useState(false); const [projectNotesPanelOpen, setProjectNotesPanelOpen] = React.useState(false); const [openSidebarMenuKey, setOpenSidebarMenuKey] = React.useState(null); const [renamingFolderId, setRenamingFolderId] = React.useState(null); @@ -315,6 +318,7 @@ export const SessionSidebar: React.FC = ({ const getSessionsByDirectory = useSessionStore((state) => state.getSessionsByDirectory); const openNewSessionDraft = useSessionStore((state) => state.openNewSessionDraft); const prStatusEntries = useGitHubPrStatusStore((state) => state.entries); + const updateStore = useUpdateStore(); const tauriIpcAvailable = React.useMemo(() => isTauriShell(), []); const isDesktopShellRuntime = React.useMemo(() => isDesktopShell(), []); @@ -585,6 +589,31 @@ export const SessionSidebar: React.FC = ({ setNewWorktreeDialogOpen(true); }, []); + const handleOpenUpdateDialog = React.useCallback(() => { + const current = useUpdateStore.getState(); + if (current.available && current.info) { + setUpdateDialogOpen(true); + return; + } + + void updateStore.checkForUpdates().then(() => { + const { available, error } = useUpdateStore.getState(); + if (error) { + toast.error('Failed to check for updates', { description: error }); + return; + } + if (!available) { + toast.success('You are on the latest version'); + return; + } + setUpdateDialogOpen(true); + }); + }, [updateStore]); + + const showSidebarUpdateButton = + updateStore.available && + (updateStore.runtimeType === 'desktop' || updateStore.runtimeType === 'web'); + const deleteSession = useSessionStore((state) => state.deleteSession); const deleteSessions = useSessionStore((state) => state.deleteSessions); const archiveSession = useSessionStore((state) => state.archiveSession); @@ -1366,6 +1395,21 @@ export const SessionSidebar: React.FC = ({ onOpenSettings={() => setSettingsDialogOpen(true)} onOpenShortcuts={toggleHelpDialog} onOpenAbout={() => setAboutDialogOpen(true)} + onOpenUpdate={handleOpenUpdateDialog} + showUpdateButton={showSidebarUpdateButton} + /> + + {editingProject ? ( diff --git a/packages/ui/src/components/session/sidebar/SidebarFooter.tsx b/packages/ui/src/components/session/sidebar/SidebarFooter.tsx index 86cd66a9..4a555047 100644 --- a/packages/ui/src/components/session/sidebar/SidebarFooter.tsx +++ b/packages/ui/src/components/session/sidebar/SidebarFooter.tsx @@ -1,16 +1,25 @@ import React from 'react'; import { RiInformationLine, RiQuestionLine, RiSettings3Line } from '@remixicon/react'; +import { Button } from '@/components/ui/button'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; type Props = { onOpenSettings: () => void; onOpenShortcuts: () => void; onOpenAbout: () => void; + onOpenUpdate: () => void; + showUpdateButton?: boolean; }; const footerButtonClassName = 'inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-interactive-hover/50 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50'; -export function SidebarFooter({ onOpenSettings, onOpenShortcuts, onOpenAbout }: Props): React.ReactNode { +export function SidebarFooter({ + onOpenSettings, + onOpenShortcuts, + onOpenAbout, + onOpenUpdate, + showUpdateButton = true, +}: Props): React.ReactNode { return (
@@ -37,6 +46,17 @@ export function SidebarFooter({ onOpenSettings, onOpenShortcuts, onOpenAbout }:

About OpenChamber

+ {showUpdateButton ? ( + + ) : null}
); } diff --git a/packages/ui/src/components/views/SettingsView.tsx b/packages/ui/src/components/views/SettingsView.tsx index 700d77d6..da863eb4 100644 --- a/packages/ui/src/components/views/SettingsView.tsx +++ b/packages/ui/src/components/views/SettingsView.tsx @@ -51,7 +51,6 @@ import { UsagePage } from '@/components/sections/usage/UsagePage'; import { GitPage } from '@/components/sections/git-identities/GitPage'; import type { OpenChamberSection } from '@/components/sections/openchamber/types'; import { OpenChamberPage } from '@/components/sections/openchamber/OpenChamberPage'; -import { AboutSettings } from '@/components/sections/openchamber/AboutSettings'; import { McpIcon } from '@/components/icons/McpIcon'; import { useDeviceInfo } from '@/lib/device'; import { isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop'; @@ -568,11 +567,6 @@ export const SettingsView: React.FC = ({ onClose, forceMobile )} - {isMobile && runtimeCtx.isWeb && ( -
- -
- )} diff --git a/packages/ui/src/stores/useUpdateStore.ts b/packages/ui/src/stores/useUpdateStore.ts index 2ce066e8..877e6bd2 100644 --- a/packages/ui/src/stores/useUpdateStore.ts +++ b/packages/ui/src/stores/useUpdateStore.ts @@ -157,9 +157,43 @@ export const useUpdateStore = create()((set, get) => ({ let suggestedSec: number | null = null; if (runtime === 'desktop') { - info = await checkForDesktopUpdates(); - const sidecarInfo = await checkForWebUpdates('desktop', info?.currentVersion); + let desktopInfo = await checkForDesktopUpdates(); + set({ + checking: false, + available: desktopInfo?.available ?? false, + info: desktopInfo, + lastChecked: Date.now(), + nextCheckInSec: null, + }); + + const sidecarInfo = await checkForWebUpdates('desktop', desktopInfo?.currentVersion); suggestedSec = sidecarInfo?.nextSuggestedCheckInSec ?? null; + + if (sidecarInfo?.available && !desktopInfo?.available) { + const forcedDesktopInfo = await checkForDesktopUpdates(); + if (forcedDesktopInfo) { + desktopInfo = forcedDesktopInfo; + } + } + + if (sidecarInfo) { + const mergedInfo: UpdateInfo = { + ...(desktopInfo ?? { available: false, currentVersion: sidecarInfo.currentVersion ?? 'unknown' }), + ...sidecarInfo, + currentVersion: desktopInfo?.currentVersion ?? sidecarInfo.currentVersion ?? 'unknown', + available: sidecarInfo.available, + }; + + set({ + available: mergedInfo.available, + info: mergedInfo, + nextCheckInSec: suggestedSec, + }); + } else { + set({ nextCheckInSec: suggestedSec }); + } + + return suggestedSec; } else if (runtime === 'web') { info = await checkForWebUpdates('web'); suggestedSec = info?.nextSuggestedCheckInSec ?? null; @@ -196,6 +230,21 @@ export const useUpdateStore = create()((set, get) => ({ set({ downloading: true, error: null, progress: null }); try { + const desktopInfo = await checkForDesktopUpdates(); + if (!desktopInfo?.available) { + throw new Error('Update detected, but desktop package is not ready yet. Retry in a moment.'); + } + + set((state) => ({ + info: state.info + ? { + ...state.info, + ...desktopInfo, + available: state.info.available, + } + : desktopInfo, + })); + const ok = await downloadDesktopUpdate((progress) => { set({ progress }); });