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
This commit is contained in:
@@ -36,12 +36,14 @@ import { useSessionFolderCleanup } from './sidebar/hooks/useSessionFolderCleanup
|
|||||||
import { useStickyProjectHeaders } from './sidebar/hooks/useStickyProjectHeaders';
|
import { useStickyProjectHeaders } from './sidebar/hooks/useStickyProjectHeaders';
|
||||||
import { useGitHubPrStatusStore } from '@/stores/useGitHubPrStatusStore';
|
import { useGitHubPrStatusStore } from '@/stores/useGitHubPrStatusStore';
|
||||||
import { ProjectEditDialog } from '@/components/layout/ProjectEditDialog';
|
import { ProjectEditDialog } from '@/components/layout/ProjectEditDialog';
|
||||||
|
import { UpdateDialog } from '@/components/ui/UpdateDialog';
|
||||||
import { SessionGroupSection } from './sidebar/SessionGroupSection';
|
import { SessionGroupSection } from './sidebar/SessionGroupSection';
|
||||||
import { SidebarHeader } from './sidebar/SidebarHeader';
|
import { SidebarHeader } from './sidebar/SidebarHeader';
|
||||||
import { SidebarActivitySections } from './sidebar/SidebarActivitySections';
|
import { SidebarActivitySections } from './sidebar/SidebarActivitySections';
|
||||||
import { SidebarFooter } from './sidebar/SidebarFooter';
|
import { SidebarFooter } from './sidebar/SidebarFooter';
|
||||||
import { SidebarProjectsList } from './sidebar/SidebarProjectsList';
|
import { SidebarProjectsList } from './sidebar/SidebarProjectsList';
|
||||||
import { SessionNodeItem } from './sidebar/SessionNodeItem';
|
import { SessionNodeItem } from './sidebar/SessionNodeItem';
|
||||||
|
import { useUpdateStore } from '@/stores/useUpdateStore';
|
||||||
import type { SortableDragHandleProps } from './sidebar/sortableItems';
|
import type { SortableDragHandleProps } from './sidebar/sortableItems';
|
||||||
import {
|
import {
|
||||||
FolderDeleteConfirmDialog,
|
FolderDeleteConfirmDialog,
|
||||||
@@ -170,6 +172,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
const [expandedSessionGroups, setExpandedSessionGroups] = React.useState<Set<string>>(new Set());
|
const [expandedSessionGroups, setExpandedSessionGroups] = React.useState<Set<string>>(new Set());
|
||||||
const [hoveredProjectId, setHoveredProjectId] = React.useState<string | null>(null);
|
const [hoveredProjectId, setHoveredProjectId] = React.useState<string | null>(null);
|
||||||
const [newWorktreeDialogOpen, setNewWorktreeDialogOpen] = React.useState(false);
|
const [newWorktreeDialogOpen, setNewWorktreeDialogOpen] = React.useState(false);
|
||||||
|
const [updateDialogOpen, setUpdateDialogOpen] = React.useState(false);
|
||||||
const [projectNotesPanelOpen, setProjectNotesPanelOpen] = React.useState(false);
|
const [projectNotesPanelOpen, setProjectNotesPanelOpen] = React.useState(false);
|
||||||
const [openSidebarMenuKey, setOpenSidebarMenuKey] = React.useState<string | null>(null);
|
const [openSidebarMenuKey, setOpenSidebarMenuKey] = React.useState<string | null>(null);
|
||||||
const [renamingFolderId, setRenamingFolderId] = React.useState<string | null>(null);
|
const [renamingFolderId, setRenamingFolderId] = React.useState<string | null>(null);
|
||||||
@@ -315,6 +318,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
const getSessionsByDirectory = useSessionStore((state) => state.getSessionsByDirectory);
|
const getSessionsByDirectory = useSessionStore((state) => state.getSessionsByDirectory);
|
||||||
const openNewSessionDraft = useSessionStore((state) => state.openNewSessionDraft);
|
const openNewSessionDraft = useSessionStore((state) => state.openNewSessionDraft);
|
||||||
const prStatusEntries = useGitHubPrStatusStore((state) => state.entries);
|
const prStatusEntries = useGitHubPrStatusStore((state) => state.entries);
|
||||||
|
const updateStore = useUpdateStore();
|
||||||
|
|
||||||
const tauriIpcAvailable = React.useMemo(() => isTauriShell(), []);
|
const tauriIpcAvailable = React.useMemo(() => isTauriShell(), []);
|
||||||
const isDesktopShellRuntime = React.useMemo(() => isDesktopShell(), []);
|
const isDesktopShellRuntime = React.useMemo(() => isDesktopShell(), []);
|
||||||
@@ -585,6 +589,31 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
setNewWorktreeDialogOpen(true);
|
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 deleteSession = useSessionStore((state) => state.deleteSession);
|
||||||
const deleteSessions = useSessionStore((state) => state.deleteSessions);
|
const deleteSessions = useSessionStore((state) => state.deleteSessions);
|
||||||
const archiveSession = useSessionStore((state) => state.archiveSession);
|
const archiveSession = useSessionStore((state) => state.archiveSession);
|
||||||
@@ -1366,6 +1395,21 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
onOpenSettings={() => setSettingsDialogOpen(true)}
|
onOpenSettings={() => setSettingsDialogOpen(true)}
|
||||||
onOpenShortcuts={toggleHelpDialog}
|
onOpenShortcuts={toggleHelpDialog}
|
||||||
onOpenAbout={() => setAboutDialogOpen(true)}
|
onOpenAbout={() => setAboutDialogOpen(true)}
|
||||||
|
onOpenUpdate={handleOpenUpdateDialog}
|
||||||
|
showUpdateButton={showSidebarUpdateButton}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UpdateDialog
|
||||||
|
open={updateDialogOpen}
|
||||||
|
onOpenChange={setUpdateDialogOpen}
|
||||||
|
info={updateStore.info}
|
||||||
|
downloading={updateStore.downloading}
|
||||||
|
downloaded={updateStore.downloaded}
|
||||||
|
progress={updateStore.progress}
|
||||||
|
error={updateStore.error}
|
||||||
|
onDownload={updateStore.downloadUpdate}
|
||||||
|
onRestart={updateStore.restartToUpdate}
|
||||||
|
runtimeType={updateStore.runtimeType}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{editingProject ? (
|
{editingProject ? (
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { RiInformationLine, RiQuestionLine, RiSettings3Line } from '@remixicon/react';
|
import { RiInformationLine, RiQuestionLine, RiSettings3Line } from '@remixicon/react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onOpenSettings: () => void;
|
onOpenSettings: () => void;
|
||||||
onOpenShortcuts: () => void;
|
onOpenShortcuts: () => void;
|
||||||
onOpenAbout: () => 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';
|
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 (
|
return (
|
||||||
<div className="flex shrink-0 items-center justify-start gap-1 px-2.5 py-2">
|
<div className="flex shrink-0 items-center justify-start gap-1 px-2.5 py-2">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -37,6 +46,17 @@ export function SidebarFooter({ onOpenSettings, onOpenShortcuts, onOpenAbout }:
|
|||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="top" sideOffset={4}><p>About OpenChamber</p></TooltipContent>
|
<TooltipContent side="top" sideOffset={4}><p>About OpenChamber</p></TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
{showUpdateButton ? (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="default"
|
||||||
|
size="xs"
|
||||||
|
className="ml-auto border-[var(--status-info-border)] bg-[var(--status-info-background)] text-[var(--status-info)] hover:bg-[var(--status-info-background)]/80 hover:text-[var(--status-info)]"
|
||||||
|
onClick={onOpenUpdate}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ import { UsagePage } from '@/components/sections/usage/UsagePage';
|
|||||||
import { GitPage } from '@/components/sections/git-identities/GitPage';
|
import { GitPage } from '@/components/sections/git-identities/GitPage';
|
||||||
import type { OpenChamberSection } from '@/components/sections/openchamber/types';
|
import type { OpenChamberSection } from '@/components/sections/openchamber/types';
|
||||||
import { OpenChamberPage } from '@/components/sections/openchamber/OpenChamberPage';
|
import { OpenChamberPage } from '@/components/sections/openchamber/OpenChamberPage';
|
||||||
import { AboutSettings } from '@/components/sections/openchamber/AboutSettings';
|
|
||||||
import { McpIcon } from '@/components/icons/McpIcon';
|
import { McpIcon } from '@/components/icons/McpIcon';
|
||||||
import { useDeviceInfo } from '@/lib/device';
|
import { useDeviceInfo } from '@/lib/device';
|
||||||
import { isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop';
|
import { isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop';
|
||||||
@@ -568,11 +567,6 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isMobile && runtimeCtx.isWeb && (
|
|
||||||
<div className="px-1.5 pt-2">
|
|
||||||
<AboutSettings />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -157,9 +157,43 @@ export const useUpdateStore = create<UpdateStore>()((set, get) => ({
|
|||||||
let suggestedSec: number | null = null;
|
let suggestedSec: number | null = null;
|
||||||
|
|
||||||
if (runtime === 'desktop') {
|
if (runtime === 'desktop') {
|
||||||
info = await checkForDesktopUpdates();
|
let desktopInfo = await checkForDesktopUpdates();
|
||||||
const sidecarInfo = await checkForWebUpdates('desktop', info?.currentVersion);
|
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;
|
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') {
|
} else if (runtime === 'web') {
|
||||||
info = await checkForWebUpdates('web');
|
info = await checkForWebUpdates('web');
|
||||||
suggestedSec = info?.nextSuggestedCheckInSec ?? null;
|
suggestedSec = info?.nextSuggestedCheckInSec ?? null;
|
||||||
@@ -196,6 +230,21 @@ export const useUpdateStore = create<UpdateStore>()((set, get) => ({
|
|||||||
set({ downloading: true, error: null, progress: null });
|
set({ downloading: true, error: null, progress: null });
|
||||||
|
|
||||||
try {
|
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) => {
|
const ok = await downloadDesktopUpdate((progress) => {
|
||||||
set({ progress });
|
set({ progress });
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user