From 92eea398ea2243cc58f5489b8da730eb3738f860 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sun, 15 Mar 2026 01:45:06 +0800 Subject: [PATCH] Remove OTA update functionality --- test-results/.last-run.json | 4 + ui/src/components/layout/MainLayout.tsx | 21 - ui/src/components/layout/NavRail.tsx | 40 +- .../sections/openchamber/AboutSettings.tsx | 224 ------- .../sections/openchamber/OpenChamberPage.tsx | 9 +- ui/src/components/ui/UpdateDialog.tsx | 547 ------------------ ui/src/components/views/SettingsView.tsx | 6 - ui/src/hooks/useMenuActions.ts | 54 +- ui/src/lib/desktop.ts | 108 ---- ui/src/stores/useUpdateStore.ts | 164 ------ web/bin/cli.js | 128 ---- web/server/index.js | 197 ------- 12 files changed, 7 insertions(+), 1495 deletions(-) create mode 100644 test-results/.last-run.json delete mode 100644 ui/src/components/sections/openchamber/AboutSettings.tsx delete mode 100644 ui/src/components/ui/UpdateDialog.tsx delete mode 100644 ui/src/stores/useUpdateStore.ts diff --git a/test-results/.last-run.json b/test-results/.last-run.json new file mode 100644 index 0000000..5fca3f8 --- /dev/null +++ b/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "failed", + "failedTests": [] +} \ No newline at end of file diff --git a/ui/src/components/layout/MainLayout.tsx b/ui/src/components/layout/MainLayout.tsx index 0494bca..bb73944 100644 --- a/ui/src/components/layout/MainLayout.tsx +++ b/ui/src/components/layout/MainLayout.tsx @@ -18,7 +18,6 @@ import { MultiRunLauncher } from '@/components/multirun'; import { DrawerProvider } from '@/contexts/DrawerContext'; import { useUIStore } from '@/stores/useUIStore'; -import { useUpdateStore } from '@/stores/useUpdateStore'; import { useDeviceInfo } from '@/lib/device'; import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory'; import { cn } from '@/lib/utils'; @@ -144,26 +143,6 @@ export const MainLayout: React.FC = () => { } }, [isRightSidebarOpen, isMobile]); - // Trigger initial update check shortly after mount, then every hour. - const checkForUpdates = useUpdateStore((state) => state.checkForUpdates); - React.useEffect(() => { - const initialDelayMs = 3000; - const periodicIntervalMs = 60 * 60 * 1000; - - const timer = window.setTimeout(() => { - checkForUpdates(); - }, initialDelayMs); - - const interval = window.setInterval(() => { - checkForUpdates(); - }, periodicIntervalMs); - - return () => { - window.clearTimeout(timer); - window.clearInterval(interval); - }; - }, [checkForUpdates]); - React.useEffect(() => { const previous = useUIStore.getState().isMobile; if (previous !== isMobile) { diff --git a/ui/src/components/layout/NavRail.tsx b/ui/src/components/layout/NavRail.tsx index 0ed3536..4fd8a48 100644 --- a/ui/src/components/layout/NavRail.tsx +++ b/ui/src/components/layout/NavRail.tsx @@ -34,13 +34,11 @@ import { import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { toast } from '@/components/ui'; -import { UpdateDialog } from '@/components/ui/UpdateDialog'; import { ProjectEditDialog } from '@/components/layout/ProjectEditDialog'; import { useUIStore } from '@/stores/useUIStore'; import { useProjectsStore } from '@/stores/useProjectsStore'; import { useSessionStore } from '@/stores/useSessionStore'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; -import { useUpdateStore } from '@/stores/useUpdateStore'; import { cn, formatDirectoryName, hasModifier } from '@/lib/utils'; import { PROJECT_ICON_MAP, PROJECT_COLOR_MAP, getProjectIconImageUrl } from '@/lib/projectMeta'; import { isDesktopLocalOriginActive, isTauriShell, requestDirectoryAccess } from '@/lib/desktop'; @@ -534,10 +532,7 @@ export const NavRail: React.FC = ({ className, mobile }) => { const currentSessionId = useSessionStore((s) => s.currentSessionId); const availableWorktreesByProject = useSessionStore((s) => s.availableWorktreesByProject); - const updateStore = useUpdateStore(); - const { available: updateAvailable, downloaded: updateDownloaded } = updateStore; - const [updateDialogOpen, setUpdateDialogOpen] = React.useState(false); - const navRailInteractionBlocked = isOverlayBlockingNavRailActions || updateDialogOpen; + const tauriIpcAvailable = React.useMemo(() => isTauriShell(), []); const [editingProject, setEditingProject] = React.useState<{ id: string; @@ -548,8 +543,6 @@ export const NavRail: React.FC = ({ className, mobile }) => { iconBackground?: string | null; } | null>(null); - const tauriIpcAvailable = React.useMemo(() => isTauriShell(), []); - const formatLabel = React.useCallback( (project: ProjectEntry): string => { return ( @@ -740,7 +733,6 @@ export const NavRail: React.FC = ({ className, mobile }) => { className={cn( 'flex h-full shrink-0 flex-col bg-[var(--surface-background)] overflow-hidden', showExpandedContent ? 'items-stretch' : 'items-center', - navRailInteractionBlocked && 'pointer-events-none', className, )} style={{ width: expanded ? NAV_RAIL_EXPANDED_WIDTH : NAV_RAIL_WIDTH }} @@ -789,7 +781,6 @@ export const NavRail: React.FC = ({ className, mobile }) => {
} tooltipLabel="Add project" @@ -805,23 +796,9 @@ export const NavRail: React.FC = ({ className, mobile }) => { 'shrink-0 w-full pt-3 pb-4 flex flex-col gap-1', showExpandedContent ? 'items-stretch px-1' : 'items-center', )}> - {(updateAvailable || updateDownloaded) && ( - setUpdateDialogOpen(true)} - disabled={navRailInteractionBlocked} - ariaLabel="Update available" - icon={} - tooltipLabel="Update available" - buttonClassName={navRailActionButtonClass} - showExpandedContent={showExpandedContent} - actionTextVisible={actionTextVisible} - /> - )} - {!mobile && ( } tooltipLabel="Shortcuts" @@ -835,7 +812,6 @@ export const NavRail: React.FC = ({ className, mobile }) => { setSettingsDialogOpen(true)} - disabled={navRailInteractionBlocked} ariaLabel="Settings" icon={} tooltipLabel="Settings" @@ -850,7 +826,6 @@ export const NavRail: React.FC = ({ className, mobile }) => { {!mobile && ( @@ -883,19 +858,6 @@ export const NavRail: React.FC = ({ className, mobile }) => { onSave={handleSaveProjectEdit} /> )} - - ); }; diff --git a/ui/src/components/sections/openchamber/AboutSettings.tsx b/ui/src/components/sections/openchamber/AboutSettings.tsx deleted file mode 100644 index 5b61159..0000000 --- a/ui/src/components/sections/openchamber/AboutSettings.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import React from 'react'; -import { RiDiscordFill, RiDownloadLine, RiGithubFill, RiLoaderLine, RiTwitterXFill } from '@remixicon/react'; -import { useUpdateStore } from '@/stores/useUpdateStore'; -import { UpdateDialog } from '@/components/ui/UpdateDialog'; -import { useDeviceInfo } from '@/lib/device'; -import { toast } from '@/components/ui'; -import { cn } from '@/lib/utils'; -import { ButtonSmall } from '@/components/ui/button-small'; - -const GITHUB_URL = 'https://github.com/btriapitsyn/openchamber'; - -const MIN_CHECKING_DURATION = 800; // ms - -export const AboutSettings: React.FC = () => { - const [updateDialogOpen, setUpdateDialogOpen] = React.useState(false); - const [showChecking, setShowChecking] = React.useState(false); - const updateStore = useUpdateStore(); - const { isMobile } = useDeviceInfo(); - - const currentVersion = updateStore.info?.currentVersion || 'unknown'; - - // Track if we initiated a check to show toast on completion - const didInitiateCheck = React.useRef(false); - - // Ensure minimum visible duration for checking animation - React.useEffect(() => { - if (updateStore.checking) { - setShowChecking(true); - didInitiateCheck.current = true; - } else if (showChecking) { - const timer = setTimeout(() => { - setShowChecking(false); - // Show toast if check completed with no update available - if (didInitiateCheck.current && !updateStore.available && !updateStore.error) { - toast.success('You are on the latest version'); - didInitiateCheck.current = false; - } - }, MIN_CHECKING_DURATION); - return () => clearTimeout(timer); - } - }, [updateStore.checking, showChecking, updateStore.available, updateStore.error]); - - const isChecking = updateStore.checking || showChecking; - - // Compact mobile layout for sidebar footer - if (isMobile) { - return ( -
- {/* Version row with update status */} -
- - v{currentVersion} - - - {!updateStore.available && !updateStore.error && ( - - )} - - {!isChecking && updateStore.available && ( - - )} -
- - {updateStore.error && ( -

{updateStore.error}

- )} - - {/* Links row */} - - - -
- ); - } - - - // Desktop layout (redesigned) - return ( -
-
-

- About OpenChamber -

-
- -
-
-
- Version - {currentVersion} -
- -
- {updateStore.checking && ( -
- - Checking... -
- )} - - {!updateStore.checking && updateStore.available && ( - setUpdateDialogOpen(true)} - > - - Update to {updateStore.info?.version} - - )} - - {!updateStore.checking && !updateStore.available && !updateStore.error && ( - Up to date - )} - - updateStore.checkForUpdates()} - disabled={updateStore.checking} - > - Check for updates - -
-
- - {updateStore.error && ( -
-

{updateStore.error}

-
- )} - - -
- - -
- ); -}; diff --git a/ui/src/components/sections/openchamber/OpenChamberPage.tsx b/ui/src/components/sections/openchamber/OpenChamberPage.tsx index c5f0c8a..769c835 100644 --- a/ui/src/components/sections/openchamber/OpenChamberPage.tsx +++ b/ui/src/components/sections/openchamber/OpenChamberPage.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { OpenChamberVisualSettings } from './OpenChamberVisualSettings'; -import { AboutSettings } from './AboutSettings'; import { SessionRetentionSettings } from './SessionRetentionSettings'; import { MemoryLimitsSettings } from './MemoryLimitsSettings'; import { DefaultsSettings } from './DefaultsSettings'; @@ -11,7 +10,7 @@ import { OpenCodeCliSettings } from './OpenCodeCliSettings'; import { KeyboardShortcutsSettings } from './KeyboardShortcutsSettings'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; import { useDeviceInfo } from '@/lib/device'; -import { isVSCodeRuntime, isWebRuntime } from '@/lib/desktop'; +import { isVSCodeRuntime } from '@/lib/desktop'; import type { OpenChamberSection } from './types'; interface OpenChamberPageProps { @@ -21,7 +20,6 @@ interface OpenChamberPageProps { export const OpenChamberPage: React.FC = ({ section }) => { const { isMobile } = useDeviceInfo(); - const showAbout = isMobile && isWebRuntime(); const isVSCode = isVSCodeRuntime(); // If no section specified, show all (mobile/legacy behavior) @@ -45,11 +43,6 @@ export const OpenChamberPage: React.FC = ({ section }) =>
- {showAbout && ( -
- -
- )}
); diff --git a/ui/src/components/ui/UpdateDialog.tsx b/ui/src/components/ui/UpdateDialog.tsx deleted file mode 100644 index 948756f..0000000 --- a/ui/src/components/ui/UpdateDialog.tsx +++ /dev/null @@ -1,547 +0,0 @@ -import React, { useState, useCallback, useEffect, useMemo } from 'react'; -import { - Dialog, - DialogContent, - DialogTitle, -} from '@/components/ui/dialog'; -import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; -import { SimpleMarkdownRenderer } from '@/components/chat/MarkdownRenderer'; -import { RiCheckLine, RiClipboardLine, RiDownloadCloudLine, RiDownloadLine, RiExternalLinkLine, RiLoaderLine, RiRestartLine, RiTerminalLine } from '@remixicon/react'; -import { cn } from '@/lib/utils'; -import type { UpdateInfo, UpdateProgress } from '@/lib/desktop'; -import { copyTextToClipboard } from '@/lib/clipboard'; - -type WebUpdateState = 'idle' | 'updating' | 'restarting' | 'reconnecting' | 'error'; - -interface UpdateDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - info: UpdateInfo | null; - downloading: boolean; - downloaded: boolean; - progress: UpdateProgress | null; - error: string | null; - onDownload: () => void; - onRestart: () => void; - /** Runtime type to show different UI for desktop vs web */ - runtimeType?: 'desktop' | 'web' | 'vscode' | null; -} - -const GITHUB_RELEASES_URL = 'https://github.com/btriapitsyn/openchamber/releases'; - -type ChangelogSection = { - version: string; - date: string; - start: number; - end: number; - raw: string; -}; - -type ParsedChangelog = - | { - kind: 'raw'; - title: string; - content: string; - } - | { - kind: 'sections'; - title: string; - sections: Array<{ version: string; dateLabel: string; content: string }>; - }; - -function formatIsoDateForUI(isoDate: string): string { - const d = new Date(`${isoDate}T00:00:00`); - if (Number.isNaN(d.getTime())) { - return isoDate; - } - return new Intl.DateTimeFormat(undefined, { - year: 'numeric', - month: 'short', - day: 'numeric', - }).format(d); -} - -function stripChangelogHeading(sectionRaw: string): string { - return sectionRaw.replace(/^## \[[^\]]+\] - \d{4}-\d{2}-\d{2}\s*\n?/, '').trim(); -} - -function processChangelogMentions(content: string): string { - // Convert @username to markdown links so they can be styled via css - return content.replace(/(^|[^a-zA-Z0-9])@([a-zA-Z0-9-]+)/g, '$1[@$2](https://github.com/$2)'); -} - -function compareSemverDesc(a: string, b: string): number { - const pa = a.split('.').map((v) => Number.parseInt(v, 10)); - const pb = b.split('.').map((v) => Number.parseInt(v, 10)); - for (let i = 0; i < 3; i += 1) { - const da = Number.isFinite(pa[i]) ? (pa[i] as number) : 0; - const db = Number.isFinite(pb[i]) ? (pb[i] as number) : 0; - if (da !== db) { - return db - da; - } - } - return 0; -} - -function parseChangelogSections(body: string): ChangelogSection[] { - const re = /^## \[(\d+\.\d+\.\d+)\] - (\d{4}-\d{2}-\d{2})\s*$/gm; - const matches: Array<{ version: string; date: string; start: number }> = []; - - let m: RegExpExecArray | null; - while ((m = re.exec(body)) !== null) { - matches.push({ - version: m[1] ?? '', - date: m[2] ?? '', - start: m.index, - }); - } - - if (matches.length === 0) { - return []; - } - - return matches.map((match, idx) => { - const end = matches[idx + 1]?.start ?? body.length; - const raw = body.slice(match.start, end).trim(); - return { version: match.version, date: match.date, start: match.start, end, raw }; - }); -} - - -type InstallWebUpdateResult = { - success: boolean; - error?: string; - autoRestart?: boolean; -}; - -const WEB_UPDATE_POLL_INTERVAL_MS = 2000; -const WEB_UPDATE_MAX_WAIT_MS = 10 * 60 * 1000; - -async function installWebUpdate(): Promise { - try { - const response = await fetch('/api/openchamber/update-install', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - }); - - if (!response.ok) { - const data = await response.json().catch(() => ({})); - return { success: false, error: data.error || `Server error: ${response.status}` }; - } - - const data = await response.json().catch(() => ({})); - return { - success: true, - autoRestart: data.autoRestart !== false, - }; - } catch (error) { - return { success: false, error: error instanceof Error ? error.message : 'Failed to install update' }; - } -} - -async function isServerReachable(): Promise { - try { - const response = await fetch('/health', { - method: 'GET', - headers: { Accept: 'application/json' }, - }); - return response.ok; - } catch { - return false; - } -} - -async function waitForUpdateApplied( - previousVersion?: string, - maxAttempts = Math.ceil(WEB_UPDATE_MAX_WAIT_MS / WEB_UPDATE_POLL_INTERVAL_MS), - intervalMs = WEB_UPDATE_POLL_INTERVAL_MS, -): Promise { - for (let i = 0; i < maxAttempts; i++) { - try { - const response = await fetch('/api/openchamber/update-check', { - method: 'GET', - headers: { Accept: 'application/json' }, - }); - if (response.ok) { - const data = await response.json().catch(() => null); - if (data && data.available === false) { - return true; - } - if ( - data && - typeof data.currentVersion === 'string' && - typeof previousVersion === 'string' && - data.currentVersion !== previousVersion - ) { - return true; - } - } else if ((response.status === 401 || response.status === 403) && await isServerReachable()) { - return true; - } - } catch { - // Server may be restarting - } - await new Promise(resolve => setTimeout(resolve, intervalMs)); - } - return false; -} - -export const UpdateDialog: React.FC = ({ - open, - onOpenChange, - info, - downloading, - downloaded, - progress, - error, - onDownload, - onRestart, - runtimeType = 'desktop', -}) => { - const [copied, setCopied] = useState(false); - const [webUpdateState, setWebUpdateState] = useState('idle'); - const [webError, setWebError] = useState(null); - - const releaseUrl = info?.version - ? `${GITHUB_RELEASES_URL}/tag/v${info.version}` - : GITHUB_RELEASES_URL; - - const progressPercent = progress?.total - ? Math.round((progress.downloaded / progress.total) * 100) - : 0; - - const isWebRuntime = runtimeType === 'web'; - const updateCommand = info?.updateCommand || 'openchamber update'; - - // Reset state when dialog closes - useEffect(() => { - if (!open) { - setWebUpdateState('idle'); - setWebError(null); - } - }, [open]); - - const handleCopyCommand = async () => { - const result = await copyTextToClipboard(updateCommand); - if (result.ok) { - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } - }; - - const handleOpenExternal = useCallback(async (url: string) => { - if (typeof window === 'undefined') return; - - // Try Tauri backend - type TauriShell = { shell?: { open?: (url: string) => Promise } }; - const tauri = (window as unknown as { __TAURI__?: TauriShell }).__TAURI__; - if (tauri?.shell?.open) { - try { - await tauri.shell.open(url); - return; - } catch { - // fall through to window.open - } - } - - try { - window.open(url, '_blank', 'noopener,noreferrer'); - } catch { - // ignore - } - }, []); - const handleWebUpdate = useCallback(async () => { - setWebUpdateState('updating'); - setWebError(null); - - const result = await installWebUpdate(); - - if (!result.success) { - setWebUpdateState('error'); - setWebError(result.error || 'Update failed'); - return; - } - - if (result.autoRestart) { - setWebUpdateState('restarting'); - await new Promise(resolve => setTimeout(resolve, 2000)); - } - - setWebUpdateState('reconnecting'); - - const applied = await waitForUpdateApplied(info?.currentVersion); - - if (applied) { - window.location.reload(); - } else { - setWebUpdateState('error'); - setWebError('Update is taking longer than expected. Wait a bit and refresh, or run: openchamber update'); - } - }, [info?.currentVersion]); - - const isWebUpdating = webUpdateState !== 'idle' && webUpdateState !== 'error'; - - const changelog = useMemo(() => { - if (!info?.body) { - return null; - } - - const body = info.body.trim(); - if (!body) { - return null; - } - - const sections = parseChangelogSections(body); - - if (sections.length === 0) { - return { - kind: 'raw', - title: "What's new", - content: processChangelogMentions(body), - }; - } - - const sorted = [...sections].sort((a, b) => compareSemverDesc(a.version, b.version)); - return { - kind: 'sections', - title: "What's new", - sections: sorted.map((section) => ({ - version: section.version, - dateLabel: formatIsoDateForUI(section.date), - content: processChangelogMentions(stripChangelogHeading(section.raw) || body), - })), - }; - }, [info?.body]); - - return ( - - - - {/* Header Section */} -
- - - - {webUpdateState === 'restarting' || webUpdateState === 'reconnecting' - ? 'Updating OpenChamber...' - : 'Update Available'} - - - - {/* Version Diff */} - {(info?.currentVersion || info?.version) && ( -
- {info?.currentVersion && ( - {info.currentVersion} - )} - {info?.currentVersion && info?.version && ( - - )} - {info?.version && ( - {info.version} - )} -
- )} -
- - {/* Content Body */} -
- - {/* Web update progress */} - {isWebRuntime && isWebUpdating && ( -
-
- -
- {webUpdateState === 'updating' && 'Installing update...'} - {webUpdateState === 'restarting' && 'Server restarting...'} - {webUpdateState === 'reconnecting' && 'Waiting for server...'} -
-
-

- The page will reload automatically when the update is complete. -

-
- )} - - {/* Changelog Rendering */} - {changelog && !isWebUpdating && ( -
- - {changelog.kind === 'raw' ? ( -
{ - const target = e.target as HTMLElement; - const a = target.closest('a'); - if (a && a.href) { - e.preventDefault(); - e.stopPropagation(); - void handleOpenExternal(a.href); - } - }} - > - -
- ) : ( -
- {changelog.sections.map((section) => ( -
-
- - v{section.version} - - - {section.dateLabel} - -
-
{ - const target = e.target as HTMLElement; - const a = target.closest('a'); - if (a && a.href) { - e.preventDefault(); - e.stopPropagation(); - void handleOpenExternal(a.href); - } - }} - > - -
-
- ))} -
- )} -
-
- )} - - {/* Web runtime fallback command */} - {isWebRuntime && webUpdateState === 'error' && ( -
-
- - Or update via terminal: -
-
- - {updateCommand} - - -
-
- )} - - {/* Desktop progress bar */} - {!isWebRuntime && downloading && ( -
-
- Downloading update payload... - {progressPercent}% -
-
-
-
-
- )} - - {/* Error display */} - {(error || webError) && ( -
-

{error || webError}

-
- )} -
- - {/* Action Footer */} -
- - - GitHub - - -
- {/* Desktop Buttons */} - {!isWebRuntime && !downloaded && !downloading && ( - - )} - - {!isWebRuntime && downloading && ( - - )} - - {!isWebRuntime && downloaded && ( - - )} - - {/* Web Buttons */} - {isWebRuntime && !isWebUpdating && ( - - )} - - {isWebRuntime && isWebUpdating && ( - - )} -
-
- -
- ); -}; diff --git a/ui/src/components/views/SettingsView.tsx b/ui/src/components/views/SettingsView.tsx index b8d2362..572bdb6 100644 --- a/ui/src/components/views/SettingsView.tsx +++ b/ui/src/components/views/SettingsView.tsx @@ -49,7 +49,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'; @@ -548,11 +547,6 @@ export const SettingsView: React.FC = ({ onClose, forceMobile )} - {isMobile && runtimeCtx.isWeb && ( -
- -
- )} diff --git a/ui/src/hooks/useMenuActions.ts b/ui/src/hooks/useMenuActions.ts index d71ac08..9d07da8 100644 --- a/ui/src/hooks/useMenuActions.ts +++ b/ui/src/hooks/useMenuActions.ts @@ -3,7 +3,6 @@ import { toast } from '@/components/ui'; import { useSessionStore } from '@/stores/useSessionStore'; import { useUIStore } from '@/stores/useUIStore'; import { useProjectsStore } from '@/stores/useProjectsStore'; -import { useUpdateStore } from '@/stores/useUpdateStore'; import { useThemeSystem } from '@/contexts/useThemeSystem'; import { sessionEvents } from '@/lib/sessionEvents'; import { isTauriShell } from '@/lib/desktop'; @@ -12,7 +11,6 @@ import { createWorktreeSession } from '@/lib/worktreeSessionCreator'; import { showOpenCodeStatus } from '@/lib/openCodeStatus'; const MENU_ACTION_EVENT = 'openchamber:menu-action'; -const CHECK_FOR_UPDATES_EVENT = 'openchamber:check-for-updates'; type TauriEventApi = { listen?: ( @@ -59,35 +57,8 @@ export const useMenuActions = ( setAboutDialogOpen, } = useUIStore(); const { addProject } = useProjectsStore(); - const checkForUpdates = useUpdateStore((state) => state.checkForUpdates); const { requestAccess, startAccessing } = useFileSystemAccess(); const { setThemeMode } = useThemeSystem(); - const checkUpdatesInFlightRef = React.useRef(false); - - const handleCheckForUpdates = React.useCallback(() => { - if (checkUpdatesInFlightRef.current) { - return; - } - checkUpdatesInFlightRef.current = true; - - void 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'); - } - }) - .finally(() => { - checkUpdatesInFlightRef.current = false; - }); - }, [checkForUpdates]); const handleChangeWorkspace = React.useCallback(() => { if (isTauriShell()) { @@ -244,17 +215,11 @@ export const useMenuActions = ( handleAction(action); }; - const handleCheckForUpdatesEvent = () => { - handleCheckForUpdates(); - }; - window.addEventListener(MENU_ACTION_EVENT, handleMenuAction); - window.addEventListener(CHECK_FOR_UPDATES_EVENT, handleCheckForUpdatesEvent); return () => { window.removeEventListener(MENU_ACTION_EVENT, handleMenuAction); - window.removeEventListener(CHECK_FOR_UPDATES_EVENT, handleCheckForUpdatesEvent); }; - }, [handleAction, handleCheckForUpdates]); + }, [handleAction]); React.useEffect(() => { if (typeof window === 'undefined') return; @@ -263,7 +228,6 @@ export const useMenuActions = ( if (typeof listen !== 'function') return; let unlistenMenu: null | (() => void | Promise) = null; - let unlistenUpdate: null | (() => void | Promise) = null; listen('openchamber:menu-action', (evt) => { const action = evt?.payload; @@ -277,16 +241,6 @@ export const useMenuActions = ( // ignore }); - listen('openchamber:check-for-updates', () => { - window.dispatchEvent(new Event(CHECK_FOR_UPDATES_EVENT)); - }) - .then((fn) => { - unlistenUpdate = fn; - }) - .catch(() => { - // ignore - }); - return () => { const cleanup = async () => { try { @@ -295,12 +249,6 @@ export const useMenuActions = ( } catch { // ignore } - try { - const b = unlistenUpdate?.(); - if (b instanceof Promise) await b; - } catch { - // ignore - } }; void cleanup(); }; diff --git a/ui/src/lib/desktop.ts b/ui/src/lib/desktop.ts index fa02103..efc1cab 100644 --- a/ui/src/lib/desktop.ts +++ b/ui/src/lib/desktop.ts @@ -5,22 +5,6 @@ export type AssistantNotificationPayload = { body?: string; }; -export type UpdateInfo = { - available: boolean; - version?: string; - currentVersion: string; - body?: string; - date?: string; - // Web-specific fields - packageManager?: string; - updateCommand?: string; -}; - -export type UpdateProgress = { - downloaded: number; - total?: number; -}; - export type SkillCatalogConfig = { id: string; label: string; @@ -309,98 +293,6 @@ export const sendAssistantCompletionNotification = async ( return false; }; -export const checkForDesktopUpdates = async (): Promise => { - if (!isTauriShell() || !isDesktopLocalOriginActive()) { - return null; - } - - try { - const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__; - const info = await tauri?.core?.invoke?.('desktop_check_for_updates'); - return info as UpdateInfo; - } catch (error) { - console.warn('Failed to check for updates (tauri)', error); - return null; - } -}; - -export const downloadDesktopUpdate = async ( - onProgress?: (progress: UpdateProgress) => void -): Promise => { - if (!isTauriShell() || !isDesktopLocalOriginActive()) { - return false; - } - - const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__; - let unlisten: null | (() => void | Promise) = null; - let downloaded = 0; - let total: number | undefined; - - try { - if (typeof onProgress === 'function' && tauri?.event?.listen) { - unlisten = await tauri.event.listen('openchamber:update-progress', (evt) => { - const payload = evt?.payload; - if (!payload || typeof payload !== 'object') return; - const data = payload as { event?: unknown; data?: unknown }; - const eventName = typeof data.event === 'string' ? data.event : null; - const eventData = data.data && typeof data.data === 'object' ? (data.data as Record) : null; - - if (eventName === 'Started') { - downloaded = 0; - total = typeof eventData?.contentLength === 'number' ? (eventData.contentLength as number) : undefined; - onProgress({ downloaded, total }); - return; - } - - if (eventName === 'Progress') { - const d = eventData?.downloaded; - const t = eventData?.total; - if (typeof d === 'number') downloaded = d; - if (typeof t === 'number') total = t; - onProgress({ downloaded, total }); - return; - } - - if (eventName === 'Finished') { - onProgress({ downloaded, total }); - } - }); - } - - await tauri?.core?.invoke?.('desktop_download_and_install_update'); - return true; - } catch (error) { - console.warn('Failed to download update (tauri)', error); - return false; - } finally { - if (unlisten) { - try { - const result = unlisten(); - if (result instanceof Promise) { - await result; - } - } catch { - // ignored - } - } - } -}; - -export const restartToApplyUpdate = async (): Promise => { - if (!isTauriShell() || !isDesktopLocalOriginActive()) { - return false; - } - - try { - const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__; - await tauri?.core?.invoke?.('desktop_restart'); - return true; - } catch (error) { - console.warn('Failed to restart for update (tauri)', error); - return false; - } -}; - export const openDesktopPath = async (path: string, app?: string | null): Promise => { if (!isTauriShell() || !isDesktopLocalOriginActive()) { return false; diff --git a/ui/src/stores/useUpdateStore.ts b/ui/src/stores/useUpdateStore.ts deleted file mode 100644 index 7f5a793..0000000 --- a/ui/src/stores/useUpdateStore.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { create } from 'zustand'; -import type { UpdateInfo, UpdateProgress } from '@/lib/desktop'; -import { - checkForDesktopUpdates, - downloadDesktopUpdate, - restartToApplyUpdate, - isDesktopLocalOriginActive, - isTauriShell, - isWebRuntime, -} from '@/lib/desktop'; - -export type UpdateState = { - checking: boolean; - available: boolean; - downloading: boolean; - downloaded: boolean; - info: UpdateInfo | null; - progress: UpdateProgress | null; - error: string | null; - runtimeType: 'desktop' | 'web' | 'vscode' | null; - lastChecked: number | null; -}; - -interface UpdateStore extends UpdateState { - checkForUpdates: () => Promise; - downloadUpdate: () => Promise; - restartToUpdate: () => Promise; - dismiss: () => void; - reset: () => void; -} - -async function checkForWebUpdates(): Promise { - try { - const response = await fetch('/api/openchamber/update-check', { - method: 'GET', - headers: { Accept: 'application/json' }, - }); - - if (!response.ok) { - throw new Error(`Server responded with ${response.status}`); - } - - const data = await response.json(); - return { - available: data.available ?? false, - version: data.version, - currentVersion: data.currentVersion ?? 'unknown', - body: data.body, - packageManager: data.packageManager, - updateCommand: data.updateCommand, - }; - } catch (error) { - console.warn('Failed to check for web updates:', error); - return null; - } -} - -function detectRuntimeType(): 'desktop' | 'web' | 'vscode' | null { - if (isTauriShell()) { - // Only use Tauri updater when we're on the local instance. - // When viewing a remote host inside the desktop shell, treat update as web update. - return isDesktopLocalOriginActive() ? 'desktop' : 'web'; - } - if (isWebRuntime()) return 'web'; - return null; -} - -const initialState: UpdateState = { - checking: false, - available: false, - downloading: false, - downloaded: false, - info: null, - progress: null, - error: null, - runtimeType: null, - lastChecked: null, -}; - -export const useUpdateStore = create()((set, get) => ({ - ...initialState, - - checkForUpdates: async () => { - const runtime = detectRuntimeType(); - if (!runtime) return; - - set({ checking: true, error: null, runtimeType: runtime }); - - try { - let info: UpdateInfo | null = null; - - if (runtime === 'desktop') { - info = await checkForDesktopUpdates(); - } else if (runtime === 'web') { - info = await checkForWebUpdates(); - } - - set({ - checking: false, - available: info?.available ?? false, - info, - lastChecked: Date.now(), - }); - } catch (error) { - set({ - checking: false, - error: error instanceof Error ? error.message : 'Failed to check for updates', - }); - } - }, - - downloadUpdate: async () => { - const { available, runtimeType } = get(); - - // For web runtime, there's no download - user uses in-app update or CLI - if (runtimeType !== 'desktop' || !available) { - return; - } - - set({ downloading: true, error: null, progress: null }); - - try { - const ok = await downloadDesktopUpdate((progress) => { - set({ progress }); - }); - if (!ok) { - throw new Error('Desktop update only works on Local instance'); - } - set({ downloading: false, downloaded: true }); - } catch (error) { - set({ - downloading: false, - error: error instanceof Error ? error.message : 'Failed to download update', - }); - } - }, - - restartToUpdate: async () => { - const { downloaded, runtimeType } = get(); - - if (runtimeType !== 'desktop' || !downloaded) { - return; - } - - try { - const ok = await restartToApplyUpdate(); - if (!ok) { - throw new Error('Desktop restart only works on Local instance'); - } - } catch (error) { - set({ - error: error instanceof Error ? error.message : 'Failed to restart', - }); - } - }, - - dismiss: () => { - set({ available: false, downloaded: false, info: null }); - }, - - reset: () => { - set(initialState); - }, -})); diff --git a/web/bin/cli.js b/web/bin/cli.js index 7f7b858..e778a69 100644 --- a/web/bin/cli.js +++ b/web/bin/cli.js @@ -138,7 +138,6 @@ COMMANDS: stop Stop running instance(s) restart Stop and start the server status Show server status - update Check for and install updates OPTIONS: -p, --port Web server port (default: ${DEFAULT_PORT}) @@ -160,7 +159,6 @@ EXAMPLES: openchamber stop # Stop all running instances openchamber stop --port 3000 # Stop specific instance openchamber status # Check status - openchamber update # Update to latest version `); } @@ -862,132 +860,6 @@ const commands = { } }, - async update() { - const os = await import('os'); - const tmpDir = os.tmpdir(); - const packageManagerPath = path.join(__dirname, '..', 'server', 'lib', 'package-manager.js'); - const { - checkForUpdates, - executeUpdate, - detectPackageManager, - getCurrentVersion, - } = await importFromFilePath(packageManagerPath); - - // Check for running instances before update - let runningInstances = []; - try { - const files = fs.readdirSync(tmpDir); - const pidFiles = files.filter(file => file.startsWith('openchamber-') && file.endsWith('.pid')); - - for (const file of pidFiles) { - const port = parseInt(file.replace('openchamber-', '').replace('.pid', '')); - if (!isNaN(port)) { - const pidFilePath = path.join(tmpDir, file); - const instanceFilePath = path.join(tmpDir, `openchamber-${port}.json`); - const pid = readPidFile(pidFilePath); - - if (pid && isProcessRunning(pid)) { - const storedOptions = readInstanceOptions(instanceFilePath); - runningInstances.push({ - port, - pid, - pidFilePath, - instanceFilePath, - storedOptions: storedOptions || { port, daemon: true }, - }); - } - } - } - } catch (error) { - // Ignore - } - - console.log('Checking for updates...'); - console.log(`Current version: ${getCurrentVersion()}`); - - const updateInfo = await checkForUpdates(); - - if (updateInfo.error) { - console.error(`Error: ${updateInfo.error}`); - process.exit(1); - } - - if (!updateInfo.available) { - console.log('\nYou are running the latest version.'); - return; - } - - console.log(`\nNew version available: ${updateInfo.version}`); - - if (updateInfo.body) { - console.log('\nChangelog:'); - console.log('─'.repeat(40)); - // Simple formatting for CLI - const formatted = updateInfo.body - .replace(/^## \[(\d+\.\d+\.\d+)\] - \d{4}-\d{2}-\d{2}/gm, '\nv$1') - .replace(/^### /gm, '\n') - .replace(/^- /gm, ' • '); - console.log(formatted); - console.log('─'.repeat(40)); - } - - // Stop running instances before update - if (runningInstances.length > 0) { - console.log(`\nStopping ${runningInstances.length} running instance(s) before update...`); - for (const instance of runningInstances) { - try { - await requestServerShutdown(instance.port); - process.kill(instance.pid, 'SIGTERM'); - let attempts = 0; - while (isProcessRunning(instance.pid) && attempts < 20) { - await new Promise(resolve => setTimeout(resolve, 250)); - attempts++; - } - if (isProcessRunning(instance.pid)) { - process.kill(instance.pid, 'SIGKILL'); - } - removePidFile(instance.pidFilePath); - console.log(` Stopped instance on port ${instance.port}`); - } catch (error) { - console.warn(` Warning: Could not stop instance on port ${instance.port}`); - } - } - } - - const pm = detectPackageManager(); - console.log(`\nDetected package manager: ${pm}`); - console.log('Installing update...\n'); - - const result = executeUpdate(pm); - - if (result.success) { - console.log('\nUpdate successful!'); - - // Restart previously running instances - if (runningInstances.length > 0) { - console.log(`\nRestarting ${runningInstances.length} instance(s)...`); - for (const instance of runningInstances) { - try { - // Force daemon mode for restart after update - const restartOptions = { - ...instance.storedOptions, - daemon: true, - }; - await commands.serve(restartOptions); - console.log(` Restarted instance on port ${instance.port}`); - } catch (error) { - console.error(` Failed to restart instance on port ${instance.port}: ${error.message}`); - console.log(` Run manually: openchamber serve --port ${instance.port} --daemon`); - } - } - } - } else { - console.error('\nUpdate failed.'); - console.error(`Exit code: ${result.exitCode}`); - process.exit(1); - } - }, - }; async function main() { diff --git a/web/server/index.js b/web/server/index.js index d57d4df..15a153a 100644 --- a/web/server/index.js +++ b/web/server/index.js @@ -6675,203 +6675,6 @@ async function main(options = {}) { }); }); - app.get('/api/openchamber/update-check', async (_req, res) => { - try { - const { checkForUpdates } = await import('./lib/package-manager.js'); - const updateInfo = await checkForUpdates(); - res.json(updateInfo); - } catch (error) { - console.error('Failed to check for updates:', error); - res.status(500).json({ - available: false, - error: error instanceof Error ? error.message : 'Failed to check for updates', - }); - } - }); - - app.post('/api/openchamber/update-install', async (_req, res) => { - try { - const { spawn: spawnChild } = await import('child_process'); - const { - checkForUpdates, - getUpdateCommand, - detectPackageManager, - } = await import('./lib/package-manager.js'); - - // Verify update is available - const updateInfo = await checkForUpdates(); - if (!updateInfo.available) { - return res.status(400).json({ error: 'No update available' }); - } - - const pm = detectPackageManager(); - const updateCmd = getUpdateCommand(pm); - const isContainer = - fs.existsSync('/.dockerenv') || - Boolean(process.env.CONTAINER) || - process.env.container === 'docker'; - - if (isContainer) { - res.json({ - success: true, - message: 'Update starting, server will stay online', - version: updateInfo.version, - packageManager: pm, - autoRestart: false, - }); - - setTimeout(() => { - console.log(`\nInstalling update using ${pm} (container mode)...`); - console.log(`Running: ${updateCmd}`); - - const shell = process.platform === 'win32' ? (process.env.ComSpec || 'cmd.exe') : 'sh'; - const shellFlag = process.platform === 'win32' ? '/c' : '-c'; - const child = spawnChild(shell, [shellFlag, updateCmd], { - detached: true, - stdio: 'ignore', - env: process.env, - }); - child.unref(); - }, 500); - - return; - } - - // Get current server port for restart - const currentPort = server.address()?.port || 3000; - - // Try to read stored instance options for restart - const tmpDir = os.tmpdir(); - const instanceFilePath = path.join(tmpDir, `openchamber-${currentPort}.json`); - let storedOptions = { port: currentPort, daemon: true }; - try { - const content = await fs.promises.readFile(instanceFilePath, 'utf8'); - storedOptions = JSON.parse(content); - } catch { - // Use defaults - } - - const isWindows = process.platform === 'win32'; - - const quotePosix = (value) => `'${String(value).replace(/'/g, "'\\''")}'`; - const quoteCmd = (value) => { - const stringValue = String(value); - return `"${stringValue.replace(/"/g, '""')}"`; - }; - - // Build restart command using explicit runtime + CLI path. - // Avoids relying on `openchamber` being in PATH for service environments. - const cliPath = path.resolve(__dirname, '..', 'bin', 'cli.js'); - const restartParts = [ - isWindows ? quoteCmd(process.execPath) : quotePosix(process.execPath), - isWindows ? quoteCmd(cliPath) : quotePosix(cliPath), - 'serve', - '--port', - String(storedOptions.port), - '--daemon', - ]; - let restartCmdPrimary = restartParts.join(' '); - let restartCmdFallback = `openchamber serve --port ${storedOptions.port} --daemon`; - if (storedOptions.uiPassword) { - if (isWindows) { - // Escape for cmd.exe quoted argument - const escapedPw = storedOptions.uiPassword.replace(/"/g, '""'); - restartCmdPrimary += ` --ui-password "${escapedPw}"`; - restartCmdFallback += ` --ui-password "${escapedPw}"`; - } else { - // Escape for POSIX single-quoted argument - const escapedPw = storedOptions.uiPassword.replace(/'/g, "'\\''"); - restartCmdPrimary += ` --ui-password '${escapedPw}'`; - restartCmdFallback += ` --ui-password '${escapedPw}'`; - } - } - const restartCmd = `(${restartCmdPrimary}) || (${restartCmdFallback})`; - - // Respond immediately - update will happen after response - res.json({ - success: true, - message: 'Update starting, server will restart shortly', - version: updateInfo.version, - packageManager: pm, - autoRestart: true, - }); - - // Give time for response to be sent - setTimeout(() => { - console.log(`\nInstalling update using ${pm}...`); - console.log(`Running: ${updateCmd}`); - - // Create a script that will: - // 1. Wait for current process to exit - // 2. Run the update - // 3. Restart the server with original options - const shell = isWindows ? (process.env.ComSpec || 'cmd.exe') : 'sh'; - const shellFlag = isWindows ? '/c' : '-c'; - const script = isWindows - ? ` - timeout /t 2 /nobreak >nul - ${updateCmd} - if %ERRORLEVEL% EQU 0 ( - echo Update successful, restarting OpenChamber... - ${restartCmd} - ) else ( - echo Update failed - exit /b 1 - ) - ` - : ` - sleep 2 - ${updateCmd} - if [ $? -eq 0 ]; then - echo "Update successful, restarting OpenChamber..." - ${restartCmd} - else - echo "Update failed" - exit 1 - fi - `; - - // Spawn detached shell to run update after we exit. - // Capture output to disk so restart failures are diagnosable. - const updateLogPath = path.join(OPENCHAMBER_DATA_DIR, 'update-install.log'); - let logFd = null; - try { - fs.mkdirSync(path.dirname(updateLogPath), { recursive: true }); - logFd = fs.openSync(updateLogPath, 'a'); - } catch (logError) { - console.warn('Failed to open update log file, continuing without log capture:', logError); - } - - const child = spawnChild(shell, [shellFlag, script], { - detached: true, - stdio: logFd !== null ? ['ignore', logFd, logFd] : 'ignore', - env: process.env, - }); - child.unref(); - - if (logFd !== null) { - try { - fs.closeSync(logFd); - } catch { - // ignore - } - } - - console.log('Update process spawned, shutting down server...'); - - // Give child process time to start, then exit - setTimeout(() => { - process.exit(0); - }, 500); - }, 500); - } catch (error) { - console.error('Failed to install update:', error); - res.status(500).json({ - error: error instanceof Error ? error.message : 'Failed to install update', - }); - } - }); - app.get('/api/openchamber/models-metadata', async (req, res) => { const now = Date.now();