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 }); });