Skip to content

feat: add cc-switch compatibility mode#435

Open
WhiskyChoy wants to merge 3 commits intoop7418:mainfrom
WhiskyChoy:codex/feat-cc-switch-compat
Open

feat: add cc-switch compatibility mode#435
WhiskyChoy wants to merge 3 commits intoop7418:mainfrom
WhiskyChoy:codex/feat-cc-switch-compat

Conversation

@WhiskyChoy
Copy link
Copy Markdown

@WhiskyChoy WhiskyChoy commented Apr 5, 2026

关联 Issue / Related Issue: closes #434

Summary / 概述

This PR adds an optional cc-switch compatibility mode in Settings. When enabled, CodePilot keeps reusing the built-in Claude Code / env provider, persists the selected default model back into the global default-model settings, and remembers the selected effort level so new chats start with the same cc-switch-style defaults.

这个 PR 在设置中增加了一个可选的 cc-switch 兼容模式。开启后,CodePilot 会继续复用内置的 Claude Code / env 提供商,把当前选中的默认模型回写到全局默认模型设置中,并记住已选择的 effort,让新对话从一开始就保持与 cc-switch 一致的默认状态。

Motivation / 背景

Some users manage Claude Code mainly through environment switching tools such as cc-switch. In that workflow, the effective default provider is often the shell-backed env target, and users also expect both the selected model and effort choice to persist across new chats.

有些用户主要通过 cc-switch 这类环境切换工具来管理 Claude Code。在这种工作流下,实际默认提供商通常就是依赖 shell 环境的 env,同时用户也希望已选模型和 effort 都能跨新对话延续。

Changes / 修改内容

  • add a new Settings > General toggle for cc-switch compatibility mode

  • allow app settings API to persist cc_switch_compat_mode

  • when compat mode is enabled, keep env from being auto-healed away as a stale default provider

  • persist effort in provider options and rehydrate it for both new-chat and existing-session chat views

  • persist env model changes back into the global default model settings from both new-chat and existing-session model selectors

  • expose effort metadata for built-in env fallback models even before SDK model metadata is loaded

  • hide legacy migrated empty Default providers from provider APIs so compat mode does not appear to create an extra provider

  • add English and Chinese UI copy plus targeted unit tests for compat-mode default persistence and legacy-placeholder filtering

  • 新增“设置 > 通用 > 开启 cc-switch 兼容模式”开关

  • 允许应用设置接口持久化 cc_switch_compat_mode

  • 兼容模式开启时,不再把 env 误判为需要自动修复的过期默认提供商

  • 在 provider options 中持久化 effort,并在新对话与已有会话视图里恢复它

  • 在新对话页和已有会话页切换 env 模型时,将其回写到全局默认模型设置,确保默认模型能持久化

  • 在 SDK 模型元数据尚未加载时,也为内置 env 回退模型暴露 effort 能力信息

  • 从 provider API 中隐藏历史迁移留下的空白 Default 占位提供商,避免兼容模式看起来像额外新增了一个 provider

  • 增加中英文 UI 文案,并补充针对兼容模式默认值持久化与历史占位 provider 过滤的单元测试

Test plan / 测试情况

  • npm run typecheck
  • npx tsx --test src/__tests__/unit/cc-switch-compat.test.ts src/__tests__/unit/legacy-provider-placeholder.test.ts src/__tests__/unit/stale-default-provider.test.ts
  • local API/database round-trip verification for providerId=__global__ default model persistence (default_model=opus, default_model_provider=env)
  • manual verification with a real cc-switch environment

Notes / 说明

  • I kept the validation focused on the affected coverage because npm run test:unit still has existing unrelated failures in this repo.

  • The local dev-only Next.js HMR adjustment I used while testing Electron on Windows is intentionally not included in this PR.

  • 由于仓库里仍有与本 PR 无关的旧测试失败,我把验证重点放在这次改动直接影响到的覆盖范围上。

  • 我在 Windows 下调试 Electron 时使用过一个仅本地开发需要的 Next.js HMR 调整,但它没有被包含进这个 PR。

Copilot AI review requested due to automatic review settings April 5, 2026 04:49
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 5, 2026

@WhiskyChoy is attempting to deploy a commit to the op7418's projects Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces an optional cc-switch compatibility mode that preserves env as a valid default provider target and persists/restores provider-level effort defaults so new chats start with more consistent settings.

Changes:

  • Add cc_switch_compat_mode app setting + Settings UI toggle with i18n + docs updates.
  • Adjust default-provider “auto-heal” behavior to keep env when compat mode is enabled.
  • Persist/re-hydrate provider effort via provider options API/DB, and gate first-send until compat-mode + provider options are loaded.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/types/index.ts Extends ProviderOptions with effort.
src/lib/provider-doctor.ts Treats env as a valid default provider under compat mode for diagnostics.
src/lib/db.ts Persists effort for env via settings; supports options_json for DB providers.
src/i18n/en.ts / src/i18n/zh.ts Adds UI strings for compat-mode toggle.
src/hooks/useCcSwitchCompatMode.ts New hook to load compat-mode setting client-side.
src/components/settings/GeneralSection.tsx Adds compat-mode toggle and persists it via app settings API.
src/components/chat/ChatView.tsx Rehydrates provider effort (compat mode) and gates sending on readiness.
src/app/chat/page.tsx Same as ChatView for new chat flow; gates first send.
src/app/api/settings/app/route.ts Allows persisting cc_switch_compat_mode.
src/app/api/providers/options/route.ts Documents effort support for provider options API.
src/app/api/providers/models/route.ts Skips auto-heal for env default when compat mode is enabled.
src/tests/unit/stale-default-provider.test.ts Adds tests for compat-mode default-provider behavior.
src/tests/unit/provider-options.test.ts Adds env provider options test for effort persistence.
apps/site/content/docs/en/providers.mdx / zh/providers.mdx Documents compat-mode behavior for cc-switch users.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +308 to +312
protocol: 'anthropic',
provider_type: 'anthropic',
api_key: process.env.ANTHROPIC_API_KEY || '',
base_url: process.env.ANTHROPIC_BASE_URL || '',
}
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When cc-switch compat mode treats env as the default provider, this synthetic provider only considers ANTHROPIC_API_KEY. Users authenticating via ANTHROPIC_AUTH_TOKEN (or the DB anthropic_auth_token) will be incorrectly flagged as “no API key” even though credentials are present. Consider either (1) extending the check to treat auth-token credentials as valid for env, or (2) setting a dedicated hasCredentials flag / adjusting the warning logic for env defaults.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I updated the compat-mode env path so Provider Doctor now treats ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, and the stored app auth token as valid env credentials before warning about the default provider. Fixed in 0af8419.

headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
providerId: currentProviderId || 'env',
options: { effort: effort || '' },
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleEffortChange serializes effort as effort || ''. Sending an empty string can end up persisting an invalid effort value (especially for non-env providers where options are stored as JSON) and makes it harder to distinguish “unset” from a real value. Prefer omitting the effort key when unset, or normalize '' to undefined server-side before persisting.

Suggested change
options: { effort: effort || '' },
options: effort === undefined ? {} : { effort },

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I stopped sending empty-string effort values from the client, and the provider-options API now treats null/missing as an explicit clear instead of persisting an invalid sentinel. Fixed in 0af8419.

headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
providerId: currentProviderId || 'env',
options: { effort: effort || '' },
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as ChatView: sending effort: effort || '' can persist an invalid empty-string effort value in options_json for DB providers. Prefer omitting the key when unset, or normalize empty strings to “unset” on the API side.

Suggested change
options: { effort: effort || '' },
options: effort ? { effort } : {},

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0af8419. New-chat and existing-session flows now both avoid effort: ""; they send a real effort value or an explicit clear, and the API normalizes that consistently.

Comment on lines 15 to 18
/**
* PUT /api/providers/options
* Update per-provider options. Body: { providerId, options: { thinking_mode?, context_1m? } }
* Update per-provider options. Body: { providerId, options: { thinking_mode?, context_1m?, effort? } }
*/
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This endpoint now advertises effort, but it still accepts and persists arbitrary values without validation/normalization. It would be safer to explicitly validate effort against the supported set (and optionally treat ''/null as “unset”) before merging and writing to DB, to avoid storing invalid provider option state.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepted. I introduced a shared provider-options sanitizer so the route now validates effort against the supported set and rejects malformed values instead of storing them. It also preserves an explicit clear path for compat mode. Fixed in 0af8419.

src/lib/db.ts Outdated
Comment on lines +1413 to +1417
const effort = getSetting('effort') || undefined;
return {
thinking_mode: thinkingMode as 'adaptive' | 'enabled' | 'disabled',
context_1m: context1m,
...(effort ? { effort: effort as import('@/types').ProviderOptions['effort'] } : {}),
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

effort is read from the settings table and type-cast without validating that the stored value is one of the supported levels. If an invalid value is written (e.g., from a malformed API call), it will be rehydrated as-if valid. Consider validating/sanitizing here (and/or in the API route) and dropping unknown effort values.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. The same shared sanitizer is now used on DB reads/writes for provider options, so invalid stored effort values are dropped during rehydration as well. I added targeted tests for invalid and clear cases in 0af8419.

Comment on lines +3 to +27
import { useCallback, useEffect, useState } from 'react';

export function useCcSwitchCompatMode(): { enabled: boolean; ready: boolean } {
const [enabled, setEnabled] = useState(false);
const [ready, setReady] = useState(false);

const fetchSetting = useCallback(() => {
setReady(false);
fetch('/api/settings/app')
.then(res => res.ok ? res.json() : null)
.then(data => {
setEnabled(data?.settings?.cc_switch_compat_mode === 'true');
})
.catch(() => {
setEnabled(false);
})
.finally(() => {
setReady(true);
});
}, []);

useEffect(() => {
fetchSetting();
window.addEventListener('app-settings-changed', fetchSetting);
return () => window.removeEventListener('app-settings-changed', fetchSetting);
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hook updates state after an async fetch without any cancellation/guard. If the component unmounts while the request is in-flight, React can warn about state updates on unmounted components. Consider using an AbortController (and ignoring AbortError) or a let cancelled = false guard in the effect.

Suggested change
import { useCallback, useEffect, useState } from 'react';
export function useCcSwitchCompatMode(): { enabled: boolean; ready: boolean } {
const [enabled, setEnabled] = useState(false);
const [ready, setReady] = useState(false);
const fetchSetting = useCallback(() => {
setReady(false);
fetch('/api/settings/app')
.then(res => res.ok ? res.json() : null)
.then(data => {
setEnabled(data?.settings?.cc_switch_compat_mode === 'true');
})
.catch(() => {
setEnabled(false);
})
.finally(() => {
setReady(true);
});
}, []);
useEffect(() => {
fetchSetting();
window.addEventListener('app-settings-changed', fetchSetting);
return () => window.removeEventListener('app-settings-changed', fetchSetting);
import { useCallback, useEffect, useRef, useState } from 'react';
export function useCcSwitchCompatMode(): { enabled: boolean; ready: boolean } {
const [enabled, setEnabled] = useState(false);
const [ready, setReady] = useState(false);
const abortControllerRef = useRef<AbortController | null>(null);
const isMountedRef = useRef(true);
const fetchSetting = useCallback(() => {
abortControllerRef.current?.abort();
const controller = new AbortController();
abortControllerRef.current = controller;
if (isMountedRef.current) {
setReady(false);
}
fetch('/api/settings/app', { signal: controller.signal })
.then(res => res.ok ? res.json() : null)
.then(data => {
if (!controller.signal.aborted && isMountedRef.current) {
setEnabled(data?.settings?.cc_switch_compat_mode === 'true');
}
})
.catch(err => {
if (err?.name === 'AbortError') {
return;
}
if (!controller.signal.aborted && isMountedRef.current) {
setEnabled(false);
}
})
.finally(() => {
if (!controller.signal.aborted && isMountedRef.current) {
setReady(true);
}
});
}, []);
useEffect(() => {
fetchSetting();
window.addEventListener('app-settings-changed', fetchSetting);
return () => {
isMountedRef.current = false;
abortControllerRef.current?.abort();
window.removeEventListener('app-settings-changed', fetchSetting);
};

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I added an abortable fetch guard in useCcSwitchCompatMode, including cleanup on unmount and on repeated refreshes, so the hook no longer updates state from a stale in-flight request. Fixed in 0af8419.

@WhiskyChoy
Copy link
Copy Markdown
Author

Follow-up update after local verification:

  • compat mode now persists env model changes back into the global default-model settings from both the new-chat page and existing chat sessions
  • built-in env fallback models now expose effort capability metadata even before SDK model metadata is loaded
  • legacy empty Default providers migrated from older settings are filtered out of provider APIs, so compat mode keeps reusing Claude Code / env instead of looking like an extra provider was created

本地补充验证后,又追加了以下修复:

  • 兼容模式下,无论是在新对话页还是已有会话页切换 env 模型,都会回写到全局默认模型设置中
  • 即使 SDK 模型元数据尚未加载,内置 env 回退模型也会正确暴露 effort 能力信息
  • 对旧版本设置迁移留下的空白 Default provider 做了过滤,避免兼容模式看起来像额外创建了一个 provider;实际仍然复用 Claude Code / env

Extra validation / 补充验证:

  • npm run typecheck
  • npx tsx --test src/__tests__/unit/cc-switch-compat.test.ts src/__tests__/unit/legacy-provider-placeholder.test.ts src/__tests__/unit/stale-default-provider.test.ts
  • local API/database round-trip check for default_model + default_model_provider

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add optional cc-switch compatibility mode

2 participants