From 628148c3590e09dcb04fb205bd41880c3f295e87 Mon Sep 17 00:00:00 2001
From: kaitranntt
Date: Fri, 23 Jan 2026 10:52:01 -0500
Subject: [PATCH 01/14] fix(cliproxy): make backend switching work with version
pins and status
- Add backend param to isCLIProxyInstalled(), getCLIProxyPath(),
getInstalledCliproxyVersion(), installCliproxyVersion()
- Update getBinaryStatus() to pass backend to all helper functions
- Add getBackendLabel() helper for dynamic CLI messages
- Replace hardcoded "CLIProxy Plus" strings with dynamic labels
- Pass --backend flag through install/update command handlers
- Import CLIProxyBackend type from types.ts instead of redefining
Setting `cliproxy.backend: original` in config.yaml now correctly
uses the original backend for version pins and binary operations.
---
src/cliproxy/binary-manager.ts | 46 +++++++++++++-------
src/cliproxy/binary/index.ts | 1 +
src/cliproxy/binary/version-cache.ts | 57 ++++++++++++++++++-------
src/cliproxy/services/binary-service.ts | 39 +++++++++++------
src/commands/cliproxy-command.ts | 43 +++++++++++++------
5 files changed, 129 insertions(+), 57 deletions(-)
diff --git a/src/cliproxy/binary-manager.ts b/src/cliproxy/binary-manager.ts
index 9ae8088f..84ec01e0 100644
--- a/src/cliproxy/binary-manager.ts
+++ b/src/cliproxy/binary-manager.ts
@@ -26,9 +26,10 @@ import {
getVersionPinPath,
readInstalledVersion,
ensureBinary,
+ migrateVersionPin,
} from './binary';
-type CLIProxyBackend = 'original' | 'plus';
+import type { CLIProxyBackend } from './types';
/**
* Get backend from config or default to 'plus'
@@ -111,7 +112,11 @@ export class BinaryManager {
/** Convenience function respecting version pin */
export async function ensureCLIProxyBinary(verbose = false): Promise {
const backend = getConfiguredBackend();
- const pinnedVersion = getPinnedVersion();
+
+ // Migrate old shared pin to backend-specific location (one-time migration)
+ migrateVersionPin(backend);
+
+ const pinnedVersion = getPinnedVersion(backend);
if (pinnedVersion) {
if (verbose) console.error(`[cliproxy] Using pinned version: ${pinnedVersion}`);
return new BinaryManager(
@@ -127,27 +132,34 @@ export async function ensureCLIProxyBinary(verbose = false): Promise {
}
/** Check if CLIProxyAPI binary is installed */
-export function isCLIProxyInstalled(): boolean {
- const backend = getConfiguredBackend();
- return new BinaryManager({}, backend).isBinaryInstalled();
+export function isCLIProxyInstalled(backend?: CLIProxyBackend): boolean {
+ const effectiveBackend = backend ?? getConfiguredBackend();
+ return new BinaryManager({}, effectiveBackend).isBinaryInstalled();
}
/** Get CLIProxyAPI binary path (may not exist) */
-export function getCLIProxyPath(): string {
- const backend = getConfiguredBackend();
- return new BinaryManager({}, backend).getBinaryPath();
+export function getCLIProxyPath(backend?: CLIProxyBackend): string {
+ const effectiveBackend = backend ?? getConfiguredBackend();
+ return new BinaryManager({}, effectiveBackend).getBinaryPath();
}
/** Get installed CLIProxyAPI version from .version file */
-export function getInstalledCliproxyVersion(): string {
- const backend = getConfiguredBackend();
- return readInstalledVersion(getBackendBinDir(backend), BACKEND_CONFIG[backend].fallbackVersion);
+export function getInstalledCliproxyVersion(backend?: CLIProxyBackend): string {
+ const effectiveBackend = backend ?? getConfiguredBackend();
+ return readInstalledVersion(
+ getBackendBinDir(effectiveBackend),
+ BACKEND_CONFIG[effectiveBackend].fallbackVersion
+ );
}
/** Install a specific version of CLIProxyAPI */
-export async function installCliproxyVersion(version: string, verbose = false): Promise {
- const backend = getConfiguredBackend();
- const manager = new BinaryManager({ version, verbose, forceVersion: true }, backend);
+export async function installCliproxyVersion(
+ version: string,
+ verbose = false,
+ backend?: CLIProxyBackend
+): Promise {
+ const effectiveBackend = backend ?? getConfiguredBackend();
+ const manager = new BinaryManager({ version, verbose, forceVersion: true }, effectiveBackend);
// Check if proxy is running and stop it first
if (isProxyRunning()) {
@@ -165,8 +177,11 @@ export async function installCliproxyVersion(version: string, verbose = false):
}
if (manager.isBinaryInstalled()) {
+ const label = effectiveBackend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy';
if (verbose)
- console.log(info(`Removing existing CLIProxy Plus v${getInstalledCliproxyVersion()}`));
+ console.log(
+ info(`Removing existing ${label} v${getInstalledCliproxyVersion(effectiveBackend)}`)
+ );
manager.deleteBinary();
}
await manager.ensureBinary();
@@ -223,6 +238,7 @@ export {
savePinnedVersion,
clearPinnedVersion,
isVersionPinned,
+ migrateVersionPin,
};
export default BinaryManager;
diff --git a/src/cliproxy/binary/index.ts b/src/cliproxy/binary/index.ts
index e0336258..23614d93 100644
--- a/src/cliproxy/binary/index.ts
+++ b/src/cliproxy/binary/index.ts
@@ -25,6 +25,7 @@ export {
savePinnedVersion,
clearPinnedVersion,
isVersionPinned,
+ migrateVersionPin,
} from './version-cache';
// Version Checker
diff --git a/src/cliproxy/binary/version-cache.ts b/src/cliproxy/binary/version-cache.ts
index 63a19a56..7d120afc 100644
--- a/src/cliproxy/binary/version-cache.ts
+++ b/src/cliproxy/binary/version-cache.ts
@@ -12,6 +12,8 @@ import {
VERSION_PIN_FILE,
VersionListCache,
} from './types';
+import { DEFAULT_BACKEND } from '../platform-detector';
+import type { CLIProxyBackend } from '../types';
/**
* Get path to version cache file
@@ -21,10 +23,10 @@ export function getVersionCachePath(): string {
}
/**
- * Get path to version pin file
+ * Get path to version pin file (backend-specific)
*/
-export function getVersionPinPath(): string {
- return path.join(getBinDir(), VERSION_PIN_FILE);
+export function getVersionPinPath(backend: CLIProxyBackend = DEFAULT_BACKEND): string {
+ return path.join(getBinDir(), backend, VERSION_PIN_FILE);
}
/**
@@ -98,10 +100,10 @@ export function writeInstalledVersion(binPath: string, version: string): void {
}
/**
- * Get pinned version if one exists
+ * Get pinned version if one exists (backend-specific)
*/
-export function getPinnedVersion(): string | null {
- const pinPath = getVersionPinPath();
+export function getPinnedVersion(backend: CLIProxyBackend = DEFAULT_BACKEND): string | null {
+ const pinPath = getVersionPinPath(backend);
if (!fs.existsSync(pinPath)) {
return null;
}
@@ -113,10 +115,13 @@ export function getPinnedVersion(): string | null {
}
/**
- * Save pinned version to persist user's explicit choice
+ * Save pinned version to persist user's explicit choice (backend-specific)
*/
-export function savePinnedVersion(version: string): void {
- const pinPath = getVersionPinPath();
+export function savePinnedVersion(
+ version: string,
+ backend: CLIProxyBackend = DEFAULT_BACKEND
+): void {
+ const pinPath = getVersionPinPath(backend);
try {
fs.mkdirSync(path.dirname(pinPath), { recursive: true });
fs.writeFileSync(pinPath, version, 'utf8');
@@ -126,10 +131,10 @@ export function savePinnedVersion(version: string): void {
}
/**
- * Clear pinned version (unpin)
+ * Clear pinned version (unpin) - backend-specific
*/
-export function clearPinnedVersion(): void {
- const pinPath = getVersionPinPath();
+export function clearPinnedVersion(backend: CLIProxyBackend = DEFAULT_BACKEND): void {
+ const pinPath = getVersionPinPath(backend);
if (fs.existsSync(pinPath)) {
try {
fs.unlinkSync(pinPath);
@@ -140,10 +145,32 @@ export function clearPinnedVersion(): void {
}
/**
- * Check if a version is currently pinned
+ * Check if a version is currently pinned (backend-specific)
*/
-export function isVersionPinned(): boolean {
- return getPinnedVersion() !== null;
+export function isVersionPinned(backend: CLIProxyBackend = DEFAULT_BACKEND): boolean {
+ return getPinnedVersion(backend) !== null;
+}
+
+/**
+ * Migrate old shared version pin to backend-specific location.
+ * Called once on first run after update.
+ */
+export function migrateVersionPin(backend: CLIProxyBackend): void {
+ const oldPinPath = path.join(getBinDir(), VERSION_PIN_FILE);
+ if (!fs.existsSync(oldPinPath)) return;
+
+ try {
+ const oldVersion = fs.readFileSync(oldPinPath, 'utf8').trim();
+ if (!oldVersion) return;
+
+ // Save to new backend-specific location
+ savePinnedVersion(oldVersion, backend);
+
+ // Delete old shared file
+ fs.unlinkSync(oldPinPath);
+ } catch {
+ // Silent fail - not critical
+ }
}
// ==================== Version List Cache ====================
diff --git a/src/cliproxy/services/binary-service.ts b/src/cliproxy/services/binary-service.ts
index 7a2458f9..7202c3cb 100644
--- a/src/cliproxy/services/binary-service.ts
+++ b/src/cliproxy/services/binary-service.ts
@@ -58,10 +58,10 @@ export function getBinaryStatus(backend?: CLIProxyBackend): BinaryStatusResult {
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
const backendConfig = BACKEND_CONFIG[effectiveBackend];
return {
- installed: isCLIProxyInstalled(),
- currentVersion: getInstalledCliproxyVersion(),
- pinnedVersion: getPinnedVersion(),
- binaryPath: getCLIProxyPath(),
+ installed: isCLIProxyInstalled(effectiveBackend),
+ currentVersion: getInstalledCliproxyVersion(effectiveBackend),
+ pinnedVersion: getPinnedVersion(effectiveBackend),
+ binaryPath: getCLIProxyPath(effectiveBackend),
fallbackVersion: backendConfig.fallbackVersion,
backend: effectiveBackend,
};
@@ -100,7 +100,11 @@ export function isValidVersionFormat(version: string): boolean {
/**
* Install a specific version and pin it
*/
-export async function installVersion(version: string, verbose = false): Promise {
+export async function installVersion(
+ version: string,
+ verbose = false,
+ backend?: CLIProxyBackend
+): Promise {
if (!isValidVersionFormat(version)) {
return {
success: false,
@@ -109,9 +113,12 @@ export async function installVersion(version: string, verbose = false): Promise<
};
}
+ const effectiveBackend =
+ backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
+
try {
- await installCliproxyVersion(version, verbose);
- savePinnedVersion(version);
+ await installCliproxyVersion(version, verbose, effectiveBackend);
+ savePinnedVersion(version, effectiveBackend);
return {
success: true,
@@ -130,13 +137,19 @@ export async function installVersion(version: string, verbose = false): Promise<
/**
* Install latest version and clear any pin
*/
-export async function installLatest(verbose = false): Promise {
+export async function installLatest(
+ verbose = false,
+ backend?: CLIProxyBackend
+): Promise {
+ const effectiveBackend =
+ backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
+
try {
const latestVersion = await fetchLatestCliproxyVersion();
- const currentVersion = getInstalledCliproxyVersion();
- const wasPinned = isVersionPinned();
+ const currentVersion = getInstalledCliproxyVersion(effectiveBackend);
+ const wasPinned = isVersionPinned(effectiveBackend);
- if (isCLIProxyInstalled() && latestVersion === currentVersion && !wasPinned) {
+ if (isCLIProxyInstalled(effectiveBackend) && latestVersion === currentVersion && !wasPinned) {
return {
success: true,
version: latestVersion,
@@ -144,8 +157,8 @@ export async function installLatest(verbose = false): Promise {
};
}
- await installCliproxyVersion(latestVersion, verbose);
- clearPinnedVersion();
+ await installCliproxyVersion(latestVersion, verbose, effectiveBackend);
+ clearPinnedVersion(effectiveBackend);
return {
success: true,
diff --git a/src/commands/cliproxy-command.ts b/src/commands/cliproxy-command.ts
index 30360803..6a08c32d 100644
--- a/src/commands/cliproxy-command.ts
+++ b/src/commands/cliproxy-command.ts
@@ -111,6 +111,13 @@ function getEffectiveBackend(cliBackend?: CLIProxyBackend): CLIProxyBackend {
return config.cliproxy?.backend ?? DEFAULT_BACKEND;
}
+/**
+ * Get display label for backend
+ */
+function getBackendLabel(backend: CLIProxyBackend): string {
+ return backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy';
+}
+
interface CliproxyProfileArgs {
name?: string;
provider?: CLIProxyProfileName;
@@ -152,8 +159,10 @@ function formatModelOption(model: ModelEntry): string {
async function handleCreate(args: string[]): Promise {
await initUI();
+ const { backend } = parseBackendArg(args);
+ const effectiveBackend = getEffectiveBackend(backend);
const parsedArgs = parseProfileArgs(args);
- console.log(header('Create CLIProxy Plus Variant'));
+ console.log(header(`Create ${getBackendLabel(effectiveBackend)} Variant`));
console.log('');
// Step 1: Profile name
@@ -292,7 +301,7 @@ async function handleCreate(args: string[]): Promise {
// Create variant
console.log('');
- console.log(info('Creating CLIProxy Plus variant...'));
+ console.log(info(`Creating ${getBackendLabel(effectiveBackend)} variant...`));
const result = createVariant(name, provider, model, account);
if (!result.success) {
@@ -530,14 +539,19 @@ async function showStatus(verbose: boolean, backend: CLIProxyBackend): Promise {
- console.log(info(`Installing CLIProxy Plus v${version}...`));
+async function handleInstallVersion(
+ version: string,
+ verbose: boolean,
+ backend: CLIProxyBackend
+): Promise {
+ const label = getBackendLabel(backend);
+ console.log(info(`Installing ${label} v${version}...`));
console.log('');
- const result = await installVersion(version, verbose);
+ const result = await installVersion(version, verbose, backend);
if (!result.success) {
console.error('');
- console.error(fail(`Failed to install CLIProxy Plus v${version}`));
+ console.error(fail(`Failed to install ${label} v${version}`));
console.error(` ${result.error}`);
console.error('');
console.error('Possible causes:');
@@ -546,12 +560,12 @@ async function handleInstallVersion(version: string, verbose: boolean): Promise<
console.error(' 3. GitHub API rate limiting');
console.error('');
console.error('Check available versions at:');
- console.error(' https://github.com/router-for-me/CLIProxyAPIPlus/releases');
+ console.error(` https://github.com/${BACKEND_CONFIG[backend].repo}/releases`);
process.exit(1);
}
console.log('');
- console.log(ok(`CLIProxy Plus v${version} installed (pinned)`));
+ console.log(ok(`${label} v${version} installed (pinned)`));
console.log('');
console.log(dim('This version will be used until you run:'));
console.log(
@@ -560,10 +574,11 @@ async function handleInstallVersion(version: string, verbose: boolean): Promise<
console.log('');
}
-async function handleInstallLatest(verbose: boolean): Promise {
- console.log(info('Fetching latest CLIProxy Plus version...'));
+async function handleInstallLatest(verbose: boolean, backend: CLIProxyBackend): Promise {
+ const label = getBackendLabel(backend);
+ console.log(info(`Fetching latest ${label} version...`));
- const result = await installLatest(verbose);
+ const result = await installLatest(verbose, backend);
if (!result.success) {
console.error(fail(`Failed to install latest version: ${result.error}`));
process.exit(1);
@@ -575,7 +590,7 @@ async function handleInstallLatest(verbose: boolean): Promise {
}
console.log('');
- console.log(ok(`CLIProxy Plus updated to v${result.version}`));
+ console.log(ok(`${label} updated to v${result.version}`));
console.log(dim('Auto-update is now enabled.'));
console.log('');
}
@@ -1036,12 +1051,12 @@ export async function handleCliproxyCommand(args: string[]): Promise {
}
// Strip leading 'v' prefix and whitespace (user may type " v6.6.80-0 ")
version = version.trim().replace(/^v/, '');
- await handleInstallVersion(version, verbose);
+ await handleInstallVersion(version, verbose, effectiveBackend);
return;
}
if (remainingArgs.includes('--latest') || remainingArgs.includes('--update')) {
- await handleInstallLatest(verbose);
+ await handleInstallLatest(verbose, effectiveBackend);
return;
}
From 388ab69a970e7bbd249948f34d7ab3e7ab5ddcb9 Mon Sep 17 00:00:00 2001
From: kaitranntt
Date: Fri, 23 Jan 2026 13:20:31 -0500
Subject: [PATCH 02/14] fix(cliproxy): complete backend param propagation per
code review
- Add backend param to checkLatestVersion()
- Add backend param to isPinned(), getPinned(), clearPin() wrappers
- Use getBackendLabel() consistently in showStatus()
Addresses review feedback from PR #359.
---
src/cliproxy/services/binary-service.ts | 25 +++++++++++++++++--------
src/commands/cliproxy-command.ts | 2 +-
2 files changed, 18 insertions(+), 9 deletions(-)
diff --git a/src/cliproxy/services/binary-service.ts b/src/cliproxy/services/binary-service.ts
index 7202c3cb..6ad3f392 100644
--- a/src/cliproxy/services/binary-service.ts
+++ b/src/cliproxy/services/binary-service.ts
@@ -70,10 +70,13 @@ export function getBinaryStatus(backend?: CLIProxyBackend): BinaryStatusResult {
/**
* Check for latest version
*/
-export async function checkLatestVersion(): Promise {
+export async function checkLatestVersion(backend?: CLIProxyBackend): Promise {
+ const effectiveBackend =
+ backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
+
try {
const latestVersion = await fetchLatestCliproxyVersion();
- const currentVersion = getInstalledCliproxyVersion();
+ const currentVersion = getInstalledCliproxyVersion(effectiveBackend);
const updateAvailable = latestVersion !== currentVersion;
return {
@@ -177,20 +180,26 @@ export async function installLatest(
/**
* Check if a version is pinned
*/
-export function isPinned(): boolean {
- return isVersionPinned();
+export function isPinned(backend?: CLIProxyBackend): boolean {
+ const effectiveBackend =
+ backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
+ return isVersionPinned(effectiveBackend);
}
/**
* Get pinned version if any
*/
-export function getPinned(): string | null {
- return getPinnedVersion();
+export function getPinned(backend?: CLIProxyBackend): string | null {
+ const effectiveBackend =
+ backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
+ return getPinnedVersion(effectiveBackend);
}
/**
* Clear version pin
*/
-export function clearPin(): void {
- clearPinnedVersion();
+export function clearPin(backend?: CLIProxyBackend): void {
+ const effectiveBackend =
+ backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
+ clearPinnedVersion(effectiveBackend);
}
diff --git a/src/commands/cliproxy-command.ts b/src/commands/cliproxy-command.ts
index 6a08c32d..ebb20861 100644
--- a/src/commands/cliproxy-command.ts
+++ b/src/commands/cliproxy-command.ts
@@ -488,7 +488,7 @@ async function showStatus(verbose: boolean, backend: CLIProxyBackend): Promise
Date: Fri, 23 Jan 2026 13:25:24 -0500
Subject: [PATCH 03/14] fix(ui): display dynamic backend label in dashboard
- Add backend/backendLabel fields to CliproxyUpdateCheckResult
- Update proxy-status-widget to show backendLabel from API
- Update cliproxy page header to use backendLabel
Dashboard now shows "CLIProxy" or "CLIProxy Plus" based on configured
backend in config.yaml instead of hardcoded "CLIProxy Plus".
---
src/cliproxy/binary-manager.ts | 7 +++++++
ui/src/components/monitoring/proxy-status-widget.tsx | 2 +-
ui/src/pages/cliproxy.tsx | 4 +++-
3 files changed, 11 insertions(+), 2 deletions(-)
diff --git a/src/cliproxy/binary-manager.ts b/src/cliproxy/binary-manager.ts
index 84ec01e0..06218844 100644
--- a/src/cliproxy/binary-manager.ts
+++ b/src/cliproxy/binary-manager.ts
@@ -205,6 +205,9 @@ export interface CliproxyUpdateCheckResult {
latestVersion: string;
fromCache: boolean;
checkedAt: number;
+ // Backend info
+ backend: CLIProxyBackend;
+ backendLabel: string;
// Stability fields
isStable: boolean;
maxStableVersion: string;
@@ -223,8 +226,12 @@ export async function checkCliproxyUpdate(): Promise
? undefined
: `v${result.currentVersion} has known stability issues. Max stable: v${CLIPROXY_MAX_STABLE_VERSION}`;
+ const backendLabel = backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy';
+
return {
...result,
+ backend,
+ backendLabel,
isStable,
maxStableVersion: CLIPROXY_MAX_STABLE_VERSION,
stabilityMessage,
diff --git a/ui/src/components/monitoring/proxy-status-widget.tsx b/ui/src/components/monitoring/proxy-status-widget.tsx
index 2417b6d6..31c69a23 100644
--- a/ui/src/components/monitoring/proxy-status-widget.tsx
+++ b/ui/src/components/monitoring/proxy-status-widget.tsx
@@ -282,7 +282,7 @@ export function ProxyStatusWidget() {
isRunning ? 'bg-green-500 animate-pulse' : 'bg-muted-foreground/30'
)}
/>
- CLIProxy Plus
+ {updateCheck?.backendLabel ?? 'CLIProxy'}
{/* Right side: icon buttons when running */}
diff --git a/ui/src/pages/cliproxy.tsx b/ui/src/pages/cliproxy.tsx
index 52d2027e..ec6daa57 100644
--- a/ui/src/pages/cliproxy.tsx
+++ b/ui/src/pages/cliproxy.tsx
@@ -19,6 +19,7 @@ import { ProxyStatusWidget } from '@/components/monitoring/proxy-status-widget';
import {
useCliproxy,
useCliproxyAuth,
+ useCliproxyUpdateCheck,
useSetDefaultAccount,
useRemoveAccount,
usePauseAccount,
@@ -179,6 +180,7 @@ export function CliproxyPage() {
const queryClient = useQueryClient();
const { data: authData, isLoading: authLoading } = useCliproxyAuth();
const { data: variantsData, isFetching } = useCliproxy();
+ const { data: updateCheck } = useCliproxyUpdateCheck();
const setDefaultMutation = useSetDefaultAccount();
const removeMutation = useRemoveAccount();
const pauseMutation = usePauseAccount();
@@ -249,7 +251,7 @@ export function CliproxyPage() {
-
CLIProxy Plus
+
{updateCheck?.backendLabel ?? 'CLIProxy'}
{
@@ -102,7 +104,7 @@ export function ModelPreferencesGrid() {
- Models available through CLIProxy Plus, grouped by provider
+ Models available through {backendLabel}, grouped by provider
diff --git a/ui/src/components/layout/app-sidebar.tsx b/ui/src/components/layout/app-sidebar.tsx
index dc5da3b4..a2cf8040 100644
--- a/ui/src/components/layout/app-sidebar.tsx
+++ b/ui/src/components/layout/app-sidebar.tsx
@@ -30,6 +30,7 @@ import {
} from '@/components/ui/sidebar';
import { CcsLogo } from '@/components/shared/ccs-logo';
import { useSidebar } from '@/hooks/use-sidebar';
+import { useCliproxyUpdateCheck } from '@/hooks/use-cliproxy';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
@@ -87,6 +88,18 @@ export function AppSidebar() {
const location = useLocation();
const navigate = useNavigate();
const { state } = useSidebar();
+ const { data: updateCheck } = useCliproxyUpdateCheck();
+
+ // Dynamic label for CLIProxy based on backend
+ const cliproxyLabel = updateCheck?.backendLabel ?? 'CLIProxy';
+
+ // Helper to get dynamic label (for CLIProxy route)
+ const getItemLabel = (item: { path: string; label: string }) => {
+ if (item.path === '/cliproxy') {
+ return cliproxyLabel;
+ }
+ return item.label;
+ };
// Helper to check if a route is active (exact match)
const isRouteActive = (path: string) => location.pathname === path;
@@ -122,13 +135,13 @@ export function AppSidebar() {
{/* Click navigates to overview AND opens submenu */}
navigate(item.path)}
>
{item.icon && }
- {item.label}
+ {getItemLabel(item)}
@@ -155,12 +168,12 @@ export function AppSidebar() {
{item.icon && }
- {item.label}
+ {getItemLabel(item)}
{item.badge && (
diff --git a/ui/src/hooks/use-cliproxy.ts b/ui/src/hooks/use-cliproxy.ts
index 3d310f02..dce70f76 100644
--- a/ui/src/hooks/use-cliproxy.ts
+++ b/ui/src/hooks/use-cliproxy.ts
@@ -333,9 +333,40 @@ export function useCliproxyUpdateCheck() {
return useQuery({
queryKey: ['cliproxy-update-check'],
queryFn: () => api.cliproxy.updateCheck(),
- staleTime: 60 * 60 * 1000, // 1 hour (matches backend cache)
- refetchInterval: 60 * 60 * 1000, // Refresh every hour
- refetchOnWindowFocus: false, // Don't refresh on window focus (save API calls)
+ staleTime: 5 * 60 * 1000, // 5 minutes (reduced from 1 hour for faster backend switch response)
+ refetchInterval: 5 * 60 * 1000, // Refresh every 5 minutes
+ refetchOnWindowFocus: true, // Refetch on window focus to catch backend changes
+ });
+}
+
+// ==================== Backend Management ====================
+
+/**
+ * Hook for switching CLIProxy backend (original vs plus)
+ * Invalidates all backend-dependent queries to ensure UI consistency
+ */
+export function useUpdateBackend() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: ({ backend, force = false }: { backend: 'original' | 'plus'; force?: boolean }) =>
+ api.cliproxyServer.updateBackend(backend, force),
+ onSuccess: () => {
+ // Invalidate all queries that depend on backend setting
+ queryClient.invalidateQueries({ queryKey: ['cliproxy-update-check'] });
+ queryClient.invalidateQueries({ queryKey: ['cliproxy-versions'] });
+ queryClient.invalidateQueries({ queryKey: ['proxy-status'] });
+ queryClient.invalidateQueries({ queryKey: ['cliproxy-stats'] });
+ toast.success('Backend updated');
+ },
+ onError: (error: Error) => {
+ // Handle 409 conflict (proxy running)
+ if (error.message.includes('Proxy is running')) {
+ toast.error('Stop the proxy first to change backend');
+ } else {
+ toast.error(error.message);
+ }
+ },
});
}
diff --git a/ui/src/pages/settings/sections/proxy/index.tsx b/ui/src/pages/settings/sections/proxy/index.tsx
index 275b356f..4c1518f7 100644
--- a/ui/src/pages/settings/sections/proxy/index.tsx
+++ b/ui/src/pages/settings/sections/proxy/index.tsx
@@ -18,8 +18,8 @@ import {
Box,
AlertTriangle,
} from 'lucide-react';
-import { toast } from 'sonner';
import { useProxyConfig, useRawConfig } from '../../hooks';
+import { useUpdateBackend } from '@/hooks/use-cliproxy';
import { LocalProxyCard } from './local-proxy-card';
import { RemoteProxyCard } from './remote-proxy-card';
import { api } from '@/lib/api-client';
@@ -74,10 +74,10 @@ export default function ProxySection() {
}
};
- // Backend state (loaded from API)
+ // Backend state (loaded from API) + mutation hook for proper query invalidation
const [backend, setBackend] = useState<'original' | 'plus'>('plus');
- const [backendSaving, setBackendSaving] = useState(false);
const [hasKiroGhcpVariants, setHasKiroGhcpVariants] = useState(false);
+ const updateBackendMutation = useUpdateBackend();
// Fetch backend setting
const fetchBackend = useCallback(async () => {
@@ -100,24 +100,18 @@ export default function ProxySection() {
}
}, []);
- // Save backend setting
- const handleBackendChange = async (value: 'original' | 'plus') => {
+ // Save backend setting using mutation hook (invalidates all related queries)
+ const handleBackendChange = (value: 'original' | 'plus') => {
const previousValue = backend;
- setBackend(value);
- setBackendSaving(true);
- try {
- await api.cliproxyServer.updateBackend(value);
- } catch (err) {
- const errorMessage = err instanceof Error ? err.message : 'Failed to save backend';
- // Check if error is due to proxy running (409 conflict)
- if (errorMessage.includes('Proxy is running')) {
- toast.error('Stop the proxy first to change backend');
+ setBackend(value); // Optimistic update
+ updateBackendMutation.mutate(
+ { backend: value },
+ {
+ onError: () => {
+ setBackend(previousValue); // Rollback on error
+ },
}
- console.error('[Proxy] Failed to save backend:', err);
- setBackend(previousValue);
- } finally {
- setBackendSaving(false);
- }
+ );
};
// Log when debug mode changes (sanitize sensitive fields)
@@ -140,8 +134,10 @@ export default function ProxySection() {
useEffect(() => {
fetchConfig();
fetchRawConfig();
- fetchBackend();
- checkPlusOnlyVariants();
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- Async data fetching on mount is intended
+ void fetchBackend();
+
+ void checkPlusOnlyVariants();
}, [fetchConfig, fetchRawConfig, fetchBackend, checkPlusOnlyVariants]);
if (loading || !config) {
@@ -253,7 +249,8 @@ export default function ProxySection() {
- Configure local or remote CLIProxy Plus connection for proxy-based profiles
+ Configure local or remote {backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy'} connection
+ for proxy-based profiles
{/* Mode Toggle - Card based selection */}
@@ -277,7 +274,7 @@ export default function ProxySection() {
Local
- Run CLIProxy Plus binary on this machine
+ Run {backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy'} binary on this machine
@@ -298,7 +295,7 @@ export default function ProxySection() {
Remote
- Connect to a remote CLIProxy Plus server
+ Connect to a remote {backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy'} server
@@ -314,7 +311,7 @@ export default function ProxySection() {
{/* Plus Backend Card */}
+ {/* Proxy Status Widget - Quick access to start/stop controls */}
+ {!isRemoteMode && (
+
+
Instance Status
+
+
+ )}
+
{/* Mode Toggle - Card based selection */}
Connection Mode
@@ -307,16 +318,25 @@ export default function ProxySection() {
Backend Binary
+ {/* Warning when proxy is running - must stop to change backend */}
+ {isProxyRunning && (
+
+
+
+ Stop the running proxy above to switch backend binary.
+
+
+ )}