From 869ab3eecd97de2a84c18c5ad25fe2abf0bdb088 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Mon, 5 Jan 2026 11:41:52 -0500 Subject: [PATCH 1/9] fix(cliproxy): cap auto-update to v80 due to v81+ context bugs CLIProxyAPIPlus v81+ has context cancellation bugs causing: - Intermittent 500 errors - "context canceled" errors during streaming - Broken token refresh handling Root cause: v81 commit 7a77b23 changed refreshToken to use detached context.Background() causing race conditions. Solution: Add CLIPROXY_MAX_STABLE_VERSION constant (6.6.80-0) and clamp auto-update to this version until upstream fixes. Closes #269 --- src/cliproxy/binary/lifecycle.ts | 30 +++++++++++++++++++++++++----- src/cliproxy/platform-detector.ts | 7 +++++++ 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/cliproxy/binary/lifecycle.ts b/src/cliproxy/binary/lifecycle.ts index 30156777..effbfda5 100644 --- a/src/cliproxy/binary/lifecycle.ts +++ b/src/cliproxy/binary/lifecycle.ts @@ -10,19 +10,38 @@ import { downloadAndInstall, deleteBinary, getBinaryPath } from './installer'; import { info } from '../../utils/ui'; import { isCliproxyRunning } from '../stats-fetcher'; import { CLIPROXY_DEFAULT_PORT } from '../config-generator'; +import { CLIPROXY_MAX_STABLE_VERSION } from '../platform-detector'; /** Log helper */ function log(message: string, verbose: boolean): void { if (verbose) console.error(`[cliproxy] ${message}`); } +/** + * Clamp version to max stable if newer versions are unstable + */ +function clampToMaxStable(version: string, verbose: boolean): string { + if (isNewerVersion(version, CLIPROXY_MAX_STABLE_VERSION)) { + log(`Clamping ${version} to max stable ${CLIPROXY_MAX_STABLE_VERSION}`, verbose); + return CLIPROXY_MAX_STABLE_VERSION; + } + return version; +} + /** Handle auto-update when binary exists */ async function handleAutoUpdate(config: BinaryManagerConfig, verbose: boolean): Promise { const updateResult = await checkForUpdates(config.binPath, config.version, verbose); if (!updateResult.hasUpdate) return; + // Clamp to max stable version + const targetVersion = clampToMaxStable(updateResult.latestVersion, verbose); + if (!isNewerVersion(targetVersion, updateResult.currentVersion)) { + log(`Already at max stable version ${updateResult.currentVersion}`, verbose); + return; + } + const proxyRunning = await isCliproxyRunning(CLIPROXY_DEFAULT_PORT); - const updateMsg = `CLIProxy Plus update available: v${updateResult.currentVersion} -> v${updateResult.latestVersion}`; + const updateMsg = `CLIProxy Plus update available: v${updateResult.currentVersion} -> v${targetVersion}`; if (proxyRunning) { console.log(info(updateMsg)); @@ -32,7 +51,7 @@ async function handleAutoUpdate(config: BinaryManagerConfig, verbose: boolean): console.log(info(updateMsg)); console.log(info('Updating CLIProxy Plus...')); deleteBinary(config.binPath, verbose); - config.version = updateResult.latestVersion; + config.version = targetVersion; await downloadAndInstall(config, verbose); } } @@ -70,9 +89,10 @@ export async function ensureBinary(config: BinaryManagerConfig): Promise if (!config.forceVersion) { try { const latestVersion = await fetchLatestVersion(verbose); - if (latestVersion && isNewerVersion(latestVersion, config.version)) { - log(`Using latest version: ${latestVersion} (instead of ${config.version})`, verbose); - config.version = latestVersion; + const targetVersion = clampToMaxStable(latestVersion, verbose); + if (targetVersion && isNewerVersion(targetVersion, config.version)) { + log(`Using version: ${targetVersion} (instead of ${config.version})`, verbose); + config.version = targetVersion; } } catch { log(`Using pinned version: ${config.version}`, verbose); diff --git a/src/cliproxy/platform-detector.ts b/src/cliproxy/platform-detector.ts index 1d8333b2..bbe76688 100644 --- a/src/cliproxy/platform-detector.ts +++ b/src/cliproxy/platform-detector.ts @@ -14,6 +14,13 @@ import { PlatformInfo, SupportedOS, SupportedArch, ArchiveExtension } from './ty */ export const CLIPROXY_FALLBACK_VERSION = '6.6.40-0'; +/** + * Maximum stable version cap - prevents auto-update to known unstable releases + * v81+ has context cancellation bugs causing intermittent 500 errors + * See: https://github.com/kaitranntt/ccs/issues/269 + */ +export const CLIPROXY_MAX_STABLE_VERSION = '6.6.80-0'; + /** @deprecated Use CLIPROXY_FALLBACK_VERSION instead */ export const CLIPROXY_VERSION = CLIPROXY_FALLBACK_VERSION; From 212aef81bc68a2d0d146d410d18f7778b8c2c100 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Mon, 5 Jan 2026 11:50:06 -0500 Subject: [PATCH 2/9] fix(cliproxy): add edge case handling for version capping - Add isAboveMaxStable() helper for version stability checks - Add null/empty guard in clampToMaxStable() function - Warn users on unstable v81+ without forcing downgrade - Add context note showing latest version when it's unstable - Clamp fallback version on GitHub API failure --- src/cliproxy/binary/lifecycle.ts | 48 ++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/src/cliproxy/binary/lifecycle.ts b/src/cliproxy/binary/lifecycle.ts index effbfda5..51867a4e 100644 --- a/src/cliproxy/binary/lifecycle.ts +++ b/src/cliproxy/binary/lifecycle.ts @@ -7,7 +7,7 @@ import * as fs from 'fs'; import { BinaryManagerConfig } from '../types'; import { checkForUpdates, fetchLatestVersion, isNewerVersion } from './version-checker'; import { downloadAndInstall, deleteBinary, getBinaryPath } from './installer'; -import { info } from '../../utils/ui'; +import { info, warn } from '../../utils/ui'; import { isCliproxyRunning } from '../stats-fetcher'; import { CLIPROXY_DEFAULT_PORT } from '../config-generator'; import { CLIPROXY_MAX_STABLE_VERSION } from '../platform-detector'; @@ -18,10 +18,22 @@ function log(message: string, verbose: boolean): void { } /** - * Clamp version to max stable if newer versions are unstable + * Check if version is above max stable (known unstable) */ -function clampToMaxStable(version: string, verbose: boolean): string { - if (isNewerVersion(version, CLIPROXY_MAX_STABLE_VERSION)) { +function isAboveMaxStable(version: string): boolean { + return isNewerVersion(version, CLIPROXY_MAX_STABLE_VERSION); +} + +/** + * Clamp version to max stable if newer versions are unstable + * Returns max stable version if input is empty/invalid + */ +function clampToMaxStable(version: string | undefined, verbose: boolean): string { + if (!version) { + log(`Empty version, using max stable ${CLIPROXY_MAX_STABLE_VERSION}`, verbose); + return CLIPROXY_MAX_STABLE_VERSION; + } + if (isAboveMaxStable(version)) { log(`Clamping ${version} to max stable ${CLIPROXY_MAX_STABLE_VERSION}`, verbose); return CLIPROXY_MAX_STABLE_VERSION; } @@ -31,17 +43,32 @@ function clampToMaxStable(version: string, verbose: boolean): string { /** Handle auto-update when binary exists */ async function handleAutoUpdate(config: BinaryManagerConfig, verbose: boolean): Promise { const updateResult = await checkForUpdates(config.binPath, config.version, verbose); + const currentVersion = updateResult.currentVersion; + const latestVersion = updateResult.latestVersion; + + // Check if user is on known unstable version - inform but don't force downgrade + if (isAboveMaxStable(currentVersion)) { + console.log( + warn( + `CLIProxy Plus v${currentVersion} has known stability issues. ` + + `Stable version: v${CLIPROXY_MAX_STABLE_VERSION}` + ) + ); + console.log(info('Run "ccs cliproxy install 80" to downgrade, or wait for upstream fix')); + } + if (!updateResult.hasUpdate) return; // Clamp to max stable version - const targetVersion = clampToMaxStable(updateResult.latestVersion, verbose); - if (!isNewerVersion(targetVersion, updateResult.currentVersion)) { - log(`Already at max stable version ${updateResult.currentVersion}`, verbose); + const targetVersion = clampToMaxStable(latestVersion, verbose); + if (!isNewerVersion(targetVersion, currentVersion)) { + log(`Already at max stable version ${currentVersion}`, verbose); return; } const proxyRunning = await isCliproxyRunning(CLIPROXY_DEFAULT_PORT); - const updateMsg = `CLIProxy Plus update available: v${updateResult.currentVersion} -> v${targetVersion}`; + const latestNote = isAboveMaxStable(latestVersion) ? ` (latest v${latestVersion} unstable)` : ''; + const updateMsg = `CLIProxy Plus update: v${currentVersion} -> v${targetVersion}${latestNote}`; if (proxyRunning) { console.log(info(updateMsg)); @@ -95,7 +122,10 @@ export async function ensureBinary(config: BinaryManagerConfig): Promise config.version = targetVersion; } } catch { - log(`Using pinned version: ${config.version}`, verbose); + // API failed - use fallback but still clamp to max stable + const fallbackVersion = clampToMaxStable(config.version, verbose); + config.version = fallbackVersion; + log(`Using fallback version: ${fallbackVersion}`, verbose); } } else { log(`Force version mode: using specified version ${config.version}`, verbose); From c5621dab515ea290e2740cc1fce79e9d65081579 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Mon, 5 Jan 2026 12:10:32 -0500 Subject: [PATCH 3/9] feat(cliproxy): add dashboard UI parity for version stability Add version stability warnings to Dashboard API and UI: - Extend /api/cliproxy/update-check with isStable, maxStableVersion, stabilityMessage - Health check shows warning status for v81+ installations - UI header displays version badge with amber warning for unstable Closes: relates to #269 --- src/cliproxy/binary-manager.ts | 22 ++++++++++- src/web-server/health/cliproxy-checks.ts | 17 ++++++++ .../components/cliproxy/cliproxy-header.tsx | 39 ++++++++++++++++++- 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/src/cliproxy/binary-manager.ts b/src/cliproxy/binary-manager.ts index 51a33d45..7e52e81a 100644 --- a/src/cliproxy/binary-manager.ts +++ b/src/cliproxy/binary-manager.ts @@ -8,7 +8,7 @@ import { info, warn } from '../utils/ui'; import { getBinDir, CLIPROXY_DEFAULT_PORT } from './config-generator'; import { BinaryInfo, BinaryManagerConfig } from './types'; -import { CLIPROXY_FALLBACK_VERSION } from './platform-detector'; +import { CLIPROXY_FALLBACK_VERSION, CLIPROXY_MAX_STABLE_VERSION } from './platform-detector'; import { isProxyRunning, stopProxy } from './services/proxy-lifecycle-service'; import { waitForPortFree } from '../utils/port-utils'; import { @@ -151,11 +151,29 @@ export interface CliproxyUpdateCheckResult { latestVersion: string; fromCache: boolean; checkedAt: number; + // Stability fields + isStable: boolean; + maxStableVersion: string; + stabilityMessage?: string; } /** Check for CLIProxyAPI binary updates */ export async function checkCliproxyUpdate(): Promise { - return new BinaryManager().checkForUpdates(); + const result = await new BinaryManager().checkForUpdates(); + + // Import isNewerVersion for stability check + const { isNewerVersion } = await import('./binary/version-checker'); + const isStable = !isNewerVersion(result.currentVersion, CLIPROXY_MAX_STABLE_VERSION); + const stabilityMessage = isStable + ? undefined + : `v${result.currentVersion} has known stability issues. Max stable: v${CLIPROXY_MAX_STABLE_VERSION}`; + + return { + ...result, + isStable, + maxStableVersion: CLIPROXY_MAX_STABLE_VERSION, + stabilityMessage, + }; } // Re-export version pin functions diff --git a/src/web-server/health/cliproxy-checks.ts b/src/web-server/health/cliproxy-checks.ts index 5c1ee2f1..fb41c7af 100644 --- a/src/web-server/health/cliproxy-checks.ts +++ b/src/web-server/health/cliproxy-checks.ts @@ -15,6 +15,8 @@ import { } from '../../cliproxy'; import { getPortProcess, isCLIProxyProcess } from '../../utils/port-utils'; import type { HealthCheck } from './types'; +import { CLIPROXY_MAX_STABLE_VERSION } from '../../cliproxy/platform-detector'; +import { isNewerVersion } from '../../cliproxy/binary/version-checker'; /** * Check CLIProxy binary installation @@ -23,6 +25,21 @@ export function checkCliproxyBinary(): HealthCheck { if (isCLIProxyInstalled()) { const version = getInstalledCliproxyVersion(); const binaryPath = getCLIProxyPath(); + + // Check if version exceeds stable cap + const isUnstable = isNewerVersion(version, CLIPROXY_MAX_STABLE_VERSION); + + if (isUnstable) { + return { + id: 'cliproxy-binary', + name: 'CLIProxy Binary', + status: 'warning', + message: `v${version} (unstable)`, + details: binaryPath, + fix: `Downgrade: ccs cliproxy install ${CLIPROXY_MAX_STABLE_VERSION.replace(/-\d+$/, '')}`, + }; + } + return { id: 'cliproxy-binary', name: 'CLIProxy Binary', diff --git a/ui/src/components/cliproxy/cliproxy-header.tsx b/ui/src/components/cliproxy/cliproxy-header.tsx index a2d39178..956d37ca 100644 --- a/ui/src/components/cliproxy/cliproxy-header.tsx +++ b/ui/src/components/cliproxy/cliproxy-header.tsx @@ -6,11 +6,17 @@ import { useState, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; -import { RefreshCw, Loader2 } from 'lucide-react'; +import { RefreshCw, Loader2, AlertTriangle } from 'lucide-react'; import { useCliproxyAuth } from '@/hooks/use-cliproxy'; import { useCliproxyAuthFlow } from '@/hooks/use-cliproxy-auth-flow'; import { cn } from '@/lib/utils'; +interface VersionInfo { + currentVersion: string; + isStable: boolean; + stabilityMessage?: string; +} + interface LoginButtonProps { provider: string; displayName: string; @@ -110,6 +116,22 @@ export function CliproxyHeader({ const { data: authData } = useCliproxyAuth(); const { provider: authProvider, isAuthenticating, startAuth } = useCliproxyAuthFlow(); const lastUpdatedText = useRelativeTime(lastUpdated); + const [versionInfo, setVersionInfo] = useState(null); + + useEffect(() => { + fetch('/api/cliproxy/update-check') + .then((res) => (res.ok ? res.json() : null)) + .then((data) => { + if (data) { + setVersionInfo({ + currentVersion: data.currentVersion, + isStable: data.isStable, + stabilityMessage: data.stabilityMessage, + }); + } + }) + .catch(() => {}); // Silently fail + }, []); const providers = [ { id: 'claude', displayName: 'Claude' }, @@ -170,6 +192,21 @@ export function CliproxyHeader({ {isRunning ? 'Running' : 'Offline'} + {versionInfo && ( + + {!versionInfo.isStable && }v + {versionInfo.currentVersion} + + )} + {lastUpdatedText && ( {lastUpdatedText} )} From 8a56a43989eb26b22ba28aa091a2bcef97701a20 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Mon, 5 Jan 2026 12:46:35 -0500 Subject: [PATCH 4/9] feat(ui): add stability warning to ProxyStatusWidget - Show amber warning indicator when installed version is unstable (v81+) - Version display now shows "(unstable)" suffix with amber styling - "Downgrade" button replaces "Update" when version is unstable - Tooltip explains restart will downgrade to stable version --- .../monitoring/proxy-status-widget.tsx | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/ui/src/components/monitoring/proxy-status-widget.tsx b/ui/src/components/monitoring/proxy-status-widget.tsx index f0801c16..78734dc1 100644 --- a/ui/src/components/monitoring/proxy-status-widget.tsx +++ b/ui/src/components/monitoring/proxy-status-widget.tsx @@ -16,6 +16,7 @@ import { RotateCw, ArrowUp, Globe, + AlertTriangle, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; @@ -75,6 +76,7 @@ export function ProxyStatusWidget() { const isRunning = status?.running ?? false; const isActioning = startProxy.isPending || stopProxy.isPending; const hasUpdate = updateCheck?.hasUpdate ?? false; + const isUnstable = updateCheck?.isStable === false; // Build remote display info const remoteDisplayHost = isRemoteMode @@ -191,29 +193,35 @@ export function ProxyStatusWidget() { {/* Control buttons when running */}
- {/* Control buttons when running */} + {/* Control buttons when running: Restart | Update/Downgrade | Stop | Settings */}
+ {/* Restart button - pure restart, no version change */} + + + {/* Update/Downgrade button - version change */} + + {/* Stop button */}
+ + {/* Version Settings (collapsible) */} + + +
+ {/* Current version */} +
+ Current: + + v{updateCheck?.currentVersion} + {isUnstable && ' (unstable)'} + +
+ + {/* Version picker row */} +
+ {/* Dropdown */} + + + {/* Manual input */} + { + setManualVersion(e.target.value); + setSelectedVersion(''); + }} + className="h-7 text-xs w-24" + /> + + {/* Install button */} + +
+ + {/* Stability warning */} + {(selectedVersion || manualVersion) && + versionsData && + isNewerVersionClient( + manualVersion || selectedVersion, + versionsData.maxStableVersion + ) && ( +
+ + + Versions above {versionsData.maxStableVersion} have known stability issues + +
+ )} +
+
+
) : (
@@ -284,6 +484,38 @@ export function ProxyStatusWidget() { )}
)} + + {/* Unstable Version Confirmation Dialog */} + + + + + + Install Unstable Version? + + +

+ You are about to install v{pendingInstallVersion}, which is above + the maximum stable version{' '} + v{versionsData?.maxStableVersion || '6.6.80'}. +

+

+ This version has known stability issues and may cause unexpected behavior. +

+

Are you sure you want to proceed?

+
+
+ + Cancel + + Install Anyway + + +
+
); } diff --git a/ui/src/hooks/use-cliproxy.ts b/ui/src/hooks/use-cliproxy.ts index d4b6ec9c..13e8efee 100644 --- a/ui/src/hooks/use-cliproxy.ts +++ b/ui/src/hooks/use-cliproxy.ts @@ -304,3 +304,59 @@ export function useCliproxyUpdateCheck() { refetchOnWindowFocus: false, // Don't refresh on window focus (save API calls) }); } + +// ==================== Version Management ==================== + +export function useCliproxyVersions() { + return useQuery({ + queryKey: ['cliproxy-versions'], + queryFn: () => api.cliproxy.versions(), + staleTime: 60 * 60 * 1000, // 1 hour (matches backend cache) + refetchOnWindowFocus: false, + }); +} + +export function useInstallVersion() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ version, force }: { version: string; force?: boolean }) => + api.cliproxy.install(version, force), + onSuccess: (data) => { + if (data.requiresConfirmation) { + // Don't show toast - let caller handle confirmation dialog + return; + } + queryClient.invalidateQueries({ queryKey: ['cliproxy-versions'] }); + queryClient.invalidateQueries({ queryKey: ['cliproxy-update-check'] }); + queryClient.invalidateQueries({ queryKey: ['proxy-status'] }); + if (data.success) { + toast.success(data.message || `Installed v${data.version}`); + } else { + toast.error(data.error || 'Installation failed'); + } + }, + onError: (error: Error) => { + toast.error(error.message); + }, + }); +} + +export function useRestartProxy() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: () => api.cliproxy.restart(), + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ['proxy-status'] }); + if (data.success) { + toast.success(`Proxy restarted on port ${data.port}`); + } else { + toast.error(data.error || 'Restart failed'); + } + }, + onError: (error: Error) => { + toast.error(error.message); + }, + }); +} diff --git a/ui/src/lib/api-client.ts b/ui/src/lib/api-client.ts index a2cf98a5..55f3519f 100644 --- a/ui/src/lib/api-client.ts +++ b/ui/src/lib/api-client.ts @@ -261,6 +261,34 @@ export interface CliproxyUpdateCheckResult { checkedAt: number; // Unix timestamp of last check } +/** Available versions list from GitHub releases */ +export interface CliproxyVersionsResponse { + versions: string[]; + latestStable: string; + latest: string; + currentVersion: string; + maxStableVersion: string; + fromCache: boolean; + checkedAt: number; +} + +/** Result from installing a specific version */ +export interface CliproxyInstallResult { + success: boolean; + version?: string; + isUnstable?: boolean; + requiresConfirmation?: boolean; + message?: string; + error?: string; +} + +/** Result from restarting the proxy */ +export interface CliproxyRestartResult { + success: boolean; + port?: number; + error?: string; +} + // API export const api = { profiles: { @@ -301,6 +329,15 @@ export const api = { proxyStop: () => request('/cliproxy/proxy-stop', { method: 'POST' }), updateCheck: () => request('/cliproxy/update-check'), + // Version management + versions: () => request('/cliproxy/versions'), + install: (version: string, force?: boolean) => + request('/cliproxy/install', { + method: 'POST', + body: JSON.stringify({ version, force }), + }), + restart: () => request('/cliproxy/restart', { method: 'POST' }), + // Stats and models for Overview tab stats: () => request<{ usage: Record }>('/cliproxy/usage'), models: () => request('/cliproxy/models'), From 48d4a96a62fecde105f58bd68ca130571ef0daa4 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Mon, 5 Jan 2026 13:21:31 -0500 Subject: [PATCH 6/9] fix(ui): update/downgrade button now installs correct version The button was incorrectly calling handleRestart (pure restart) instead of triggering version install. Now correctly installs latestVersion for updates or maxStableVersion for downgrades. --- .../components/monitoring/proxy-status-widget.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/ui/src/components/monitoring/proxy-status-widget.tsx b/ui/src/components/monitoring/proxy-status-widget.tsx index ea42c35a..b7cd0a09 100644 --- a/ui/src/components/monitoring/proxy-status-widget.tsx +++ b/ui/src/components/monitoring/proxy-status-widget.tsx @@ -172,14 +172,6 @@ export function ProxyStatusWidget() { })() : null; - // Restart = stop then start - const handleRestart = async () => { - await stopProxy.mutateAsync(); - // Small delay to ensure port is released - await new Promise((r) => setTimeout(r, 500)); - startProxy.mutate(); - }; - // Remote mode: show remote server info if (isRemoteMode) { return ( @@ -303,7 +295,12 @@ export function ProxyStatusWidget() { : hasUpdate && 'bg-sidebar-accent hover:bg-sidebar-accent/90 text-sidebar-accent-foreground' )} - onClick={handleRestart} + onClick={() => { + const targetVersion = isUnstable + ? updateCheck?.maxStableVersion || versionsData?.latestStable + : updateCheck?.latestVersion; + if (targetVersion) handleInstallVersion(targetVersion); + }} disabled={isActioning || (!hasUpdate && !isUnstable)} title={ isUnstable From 8072b93b3b2d4bc721fece815c8edf45da67b34b Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Mon, 5 Jan 2026 13:49:24 -0500 Subject: [PATCH 7/9] refactor(ui): redesign ProxyStatusWidget with two-state UX - Replace 4-button row with 3 icon buttons (restart, stop, settings) - Add tooltips to all icon buttons via TooltipProvider - Move version display to header next to title - Implement collapsed/expanded two-state design - Remove manual version input field (use dropdown only) - Add clickable update/downgrade badge showing target version - Add "Version Management" section header in expanded view - Increase control sizes (h-8) for better touch targets - Settings icon toggles to X when expanded --- .../monitoring/proxy-status-widget.tsx | 557 +++++++++--------- 1 file changed, 276 insertions(+), 281 deletions(-) diff --git a/ui/src/components/monitoring/proxy-status-widget.tsx b/ui/src/components/monitoring/proxy-status-widget.tsx index b7cd0a09..a67db32f 100644 --- a/ui/src/components/monitoring/proxy-status-widget.tsx +++ b/ui/src/components/monitoring/proxy-status-widget.tsx @@ -4,6 +4,8 @@ * Displays CLIProxy process status with start/stop/restart controls. * Shows: running state, port, session count, uptime, update availability. * In remote mode: shows remote server info instead of local controls. + * + * Design: Two-state widget (collapsed/expanded) with icon-only control buttons. */ import { useState } from 'react'; @@ -20,10 +22,11 @@ import { Globe, AlertTriangle, Settings, + X, + Download, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; -import { Input } from '@/components/ui/input'; import { Collapsible, CollapsibleContent } from '@/components/ui/collapsible'; import { Select, @@ -42,6 +45,7 @@ import { AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { useQuery } from '@tanstack/react-query'; import { api, type CliproxyServerConfig } from '@/lib/api-client'; import { @@ -92,6 +96,53 @@ function formatTimeAgo(timestamp?: number): string { return `${hours}h ago`; } +/** Icon button with tooltip wrapper */ +function IconButton({ + icon: Icon, + tooltip, + onClick, + disabled, + isPending, + className, + variant = 'ghost', +}: { + icon: React.ElementType; + tooltip: string; + onClick: () => void; + disabled?: boolean; + isPending?: boolean; + className?: string; + variant?: 'ghost' | 'outline' | 'destructive-ghost'; +}) { + return ( + + + + + + {tooltip} + + + ); +} + export function ProxyStatusWidget() { const { data: status, isLoading } = useProxyStatus(); const { data: updateCheck } = useCliproxyUpdateCheck(); @@ -101,10 +152,9 @@ export function ProxyStatusWidget() { const restartProxy = useRestartProxy(); const installVersion = useInstallVersion(); - // Version picker state - const [showVersionSettings, setShowVersionSettings] = useState(false); + // Version picker state (expanded section) + const [isExpanded, setIsExpanded] = useState(false); const [selectedVersion, setSelectedVersion] = useState(''); - const [manualVersion, setManualVersion] = useState(''); // Confirmation dialog state for unstable versions const [showUnstableConfirm, setShowUnstableConfirm] = useState(false); @@ -129,6 +179,12 @@ export function ProxyStatusWidget() { installVersion.isPending; const hasUpdate = updateCheck?.hasUpdate ?? false; const isUnstable = updateCheck?.isStable === false; + const currentVersion = updateCheck?.currentVersion; + + // Target version for update/downgrade badge + const targetVersion = isUnstable + ? updateCheck?.maxStableVersion || versionsData?.latestStable + : updateCheck?.latestVersion; // Handle version install (shows confirmation for unstable) const handleInstallVersion = (version: string) => { @@ -207,49 +263,104 @@ export function ProxyStatusWidget() { ); } - // Local mode: show original controls - + // Local mode: Two-state widget (collapsed/expanded) return ( -
-
-
-
+
+ {/* Header row: Status dot, title, version, update badge, icon buttons */} +
+
+ {/* Status indicator */} +
+ CLIProxy Plus + + {/* Version in header */} + {currentVersion && ( + + v{currentVersion} + )} - /> - CLIProxy Plus - {hasUpdate && ( - v${updateCheck?.latestVersion}`} - > - - Update - - )} + + {/* Clickable Update/Downgrade badge */} + {(hasUpdate || isUnstable) && targetVersion && ( + handleInstallVersion(targetVersion)} + title={`Click to ${isUnstable ? 'downgrade' : 'update'}`} + > + {isUnstable ? ( + <> + + {targetVersion} + + ) : ( + <> + + {targetVersion} + + )} + + )} +
+ + {/* Right side: status icon + control buttons when running */} +
+ {isLoading ? ( + + ) : isRunning ? ( + <> + {/* Icon buttons: Restart, Stop, Settings/Close */} + restartProxy.mutate()} + disabled={isActioning} + isPending={restartProxy.isPending} + /> + stopProxy.mutate()} + disabled={isActioning} + isPending={stopProxy.isPending} + variant="destructive-ghost" + /> + setIsExpanded(!isExpanded)} + className={isExpanded ? 'bg-muted' : undefined} + /> + + ) : ( + + )} +
-
- {isLoading ? ( - - ) : isRunning ? ( - - ) : ( - - )} -
-
- - {isRunning && status ? ( - <> + {/* Stats row when running */} + {isRunning && status && (
Port {status.port} {status.sessionCount !== undefined && status.sessionCount > 0 && ( @@ -265,254 +376,138 @@ export function ProxyStatusWidget() { )}
- {/* Control buttons when running: Restart | Update/Downgrade | Stop | Settings */} -
- {/* Restart button - pure restart, no version change */} + )} + + {/* Expanded section: Version Management */} + {isRunning && ( + + + {/* Section header */} +

Version Management

+ + {/* Version picker row */} +
+ {/* Dropdown - full width, no truncation */} + + + {/* Install button */} + +
+ + {/* Stability warning for selected version */} + {selectedVersion && + versionsData?.maxStableVersion && + isNewerVersionClient(selectedVersion, versionsData.maxStableVersion) && ( +
+ + Versions above {versionsData.maxStableVersion} have known issues +
+ )} + + {/* Sync time */} + {updateCheck?.checkedAt && ( +
+ Last checked {formatTimeAgo(updateCheck.checkedAt)} +
+ )} +
+
+ )} + + {/* Not running state */} + {!isRunning && ( +
+ Not running - - {/* Update/Downgrade button - version change */} - - - {/* Stop button */} - - - {/* Settings gear - toggle version picker */} -
+ )} - {/* Version Settings (collapsible) */} - - -
- {/* Current version */} -
- Current: - - v{updateCheck?.currentVersion} - {isUnstable && ' (unstable)'} - -
- - {/* Version picker row */} -
- {/* Dropdown */} - - - {/* Manual input */} - { - setManualVersion(e.target.value); - setSelectedVersion(''); - }} - className="h-7 text-xs w-24" - /> - - {/* Install button */} - -
- - {/* Stability warning */} - {(selectedVersion || manualVersion) && - versionsData && - isNewerVersionClient( - manualVersion || selectedVersion, - versionsData.maxStableVersion - ) && ( -
- - - Versions above {versionsData.maxStableVersion} have known stability issues - -
- )} -
-
-
- - ) : ( -
- Not running - -
- )} - - {/* Version sync indicator */} - {updateCheck?.currentVersion && ( -
- - {isUnstable && ( - - )} - - v{updateCheck.currentVersion} - - {isUnstable && ( - (unstable) - )} - - {updateCheck.checkedAt && ( - - Synced {formatTimeAgo(updateCheck.checkedAt)} - - )} -
- )} - - {/* Unstable Version Confirmation Dialog */} - - - - - - Install Unstable Version? - - -

- You are about to install v{pendingInstallVersion}, which is above - the maximum stable version{' '} - v{versionsData?.maxStableVersion || '6.6.80'}. -

-

- This version has known stability issues and may cause unexpected behavior. -

-

Are you sure you want to proceed?

-
-
- - Cancel - - Install Anyway - - -
-
-
+ {/* Unstable Version Confirmation Dialog */} + + + + + + Install Unstable Version? + + +

+ You are about to install v{pendingInstallVersion}, which is above + the maximum stable version{' '} + v{versionsData?.maxStableVersion || '6.6.80'}. +

+

+ This version has known stability issues and may cause unexpected behavior. +

+

Are you sure you want to proceed?

+
+
+ + Cancel + + Install Anyway + + +
+
+
+ ); } From 4f69abbe88fb0bd6782373b5eeebd665dfcafd4b Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Mon, 5 Jan 2026 14:31:55 -0500 Subject: [PATCH 8/9] fix(ui): clean up ProxyStatusWidget layout spacing - Separate header row (title + icon buttons) from version row - Compact update badge: h-4, px-1.5, gap-0.5 - Slightly more margin between rows (mt-1.5) - Fix badge conditional rendering --- .../monitoring/proxy-status-widget.tsx | 78 +++++++++---------- 1 file changed, 36 insertions(+), 42 deletions(-) diff --git a/ui/src/components/monitoring/proxy-status-widget.tsx b/ui/src/components/monitoring/proxy-status-widget.tsx index a67db32f..2417b6d6 100644 --- a/ui/src/components/monitoring/proxy-status-widget.tsx +++ b/ui/src/components/monitoring/proxy-status-widget.tsx @@ -272,7 +272,7 @@ export function ProxyStatusWidget() { isRunning ? 'border-green-500/30 bg-green-500/5' : 'border-muted bg-muted/30' )} > - {/* Header row: Status dot, title, version, update badge, icon buttons */} + {/* Header row: Status dot, title, icon buttons */}
{/* Status indicator */} @@ -283,54 +283,14 @@ export function ProxyStatusWidget() { )} /> CLIProxy Plus - - {/* Version in header */} - {currentVersion && ( - - v{currentVersion} - - )} - - {/* Clickable Update/Downgrade badge */} - {(hasUpdate || isUnstable) && targetVersion && ( - handleInstallVersion(targetVersion)} - title={`Click to ${isUnstable ? 'downgrade' : 'update'}`} - > - {isUnstable ? ( - <> - - {targetVersion} - - ) : ( - <> - - {targetVersion} - - )} - - )}
- {/* Right side: status icon + control buttons when running */} + {/* Right side: icon buttons when running */}
{isLoading ? ( ) : isRunning ? ( <> - {/* Icon buttons: Restart, Stop, Settings/Close */}
+ {/* Version row: version + update badge */} + {currentVersion && ( +
+ + v{currentVersion} + + {(hasUpdate || isUnstable) && targetVersion && ( + handleInstallVersion(targetVersion)} + title={`Click to ${isUnstable ? 'downgrade' : 'update'}`} + > + {isUnstable ? ( + + ) : ( + + )} + {targetVersion} + + )} +
+ )} + {/* Stats row when running */} {isRunning && status && (
From 4fd4d6c264b5dbcb9f3e181b46a82090764f46c6 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Mon, 5 Jan 2026 14:54:27 -0500 Subject: [PATCH 9/9] fix(ui): add missing isStable and maxStableVersion to type --- ui/src/lib/api-client.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ui/src/lib/api-client.ts b/ui/src/lib/api-client.ts index 55f3519f..aae75b09 100644 --- a/ui/src/lib/api-client.ts +++ b/ui/src/lib/api-client.ts @@ -259,6 +259,9 @@ export interface CliproxyUpdateCheckResult { latestVersion: string; fromCache: boolean; checkedAt: number; // Unix timestamp of last check + isStable: boolean; // Whether current version is at or below max stable + maxStableVersion: string; // Maximum stable version (e.g., "6.6.80") + stabilityMessage?: string; // Warning message if running unstable version } /** Available versions list from GitHub releases */