mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-07-02 10:33:00 +08:00
feat: add Claude and Codex effort controls
This commit is contained in:
@@ -37,6 +37,8 @@ interface UseChatComposerStateArgs {
|
||||
cursorModel: string;
|
||||
claudeModel: string;
|
||||
codexModel: string;
|
||||
claudeEffort: string;
|
||||
codexEffort: string;
|
||||
geminiModel: string;
|
||||
opencodeModel: string;
|
||||
isLoading: boolean;
|
||||
@@ -161,6 +163,17 @@ const getNotificationSessionSummary = (
|
||||
return normalizedFallback.length > 80 ? `${normalizedFallback.slice(0, 77)}...` : normalizedFallback;
|
||||
};
|
||||
|
||||
const getLatestProviderEffort = (
|
||||
provider: LLMProvider,
|
||||
fallbackEffort: string,
|
||||
): string => {
|
||||
if (provider !== 'claude' && provider !== 'codex') {
|
||||
return 'default';
|
||||
}
|
||||
|
||||
return safeLocalStorage.getItem(`${provider}-effort`) || fallbackEffort;
|
||||
};
|
||||
|
||||
export function useChatComposerState({
|
||||
selectedProject,
|
||||
selectedSession,
|
||||
@@ -171,6 +184,8 @@ export function useChatComposerState({
|
||||
cursorModel,
|
||||
claudeModel,
|
||||
codexModel,
|
||||
claudeEffort,
|
||||
codexEffort,
|
||||
geminiModel,
|
||||
opencodeModel,
|
||||
isLoading,
|
||||
@@ -730,6 +745,12 @@ export function useChatComposerState({
|
||||
: provider === 'opencode'
|
||||
? opencodeModel
|
||||
: claudeModel;
|
||||
const effort =
|
||||
provider === 'claude'
|
||||
? getLatestProviderEffort(provider, claudeEffort)
|
||||
: provider === 'codex'
|
||||
? getLatestProviderEffort(provider, codexEffort)
|
||||
: 'default';
|
||||
|
||||
// One message shape for every provider. The backend resolves the
|
||||
// provider, project path, and provider-native resume id from the
|
||||
@@ -740,6 +761,7 @@ export function useChatComposerState({
|
||||
content: messageContent,
|
||||
options: {
|
||||
model,
|
||||
effort,
|
||||
// Codex has no plan mode; downgrade rather than sending an
|
||||
// unsupported value to its runtime.
|
||||
permissionMode: provider === 'codex' && permissionMode === 'plan' ? 'default' : permissionMode,
|
||||
@@ -768,7 +790,9 @@ export function useChatComposerState({
|
||||
selectedSession,
|
||||
attachedImages,
|
||||
claudeModel,
|
||||
claudeEffort,
|
||||
codexModel,
|
||||
codexEffort,
|
||||
currentSessionId,
|
||||
cursorModel,
|
||||
executeCommand,
|
||||
|
||||
@@ -5,18 +5,26 @@ import type {
|
||||
ProjectSession,
|
||||
LLMProvider,
|
||||
Project,
|
||||
ProviderModelOption,
|
||||
ProviderModelsCacheInfo,
|
||||
ProviderModelsDefinition,
|
||||
} from '../../../types/app';
|
||||
|
||||
const FALLBACK_DEFAULT_MODEL: Record<LLMProvider, string> = {
|
||||
claude: 'opus',
|
||||
claude: 'default',
|
||||
cursor: 'gpt-5.3-codex',
|
||||
codex: 'gpt-5.4',
|
||||
gemini: 'gemini-3.1-pro-preview',
|
||||
opencode: 'anthropic/claude-sonnet-4-5',
|
||||
};
|
||||
|
||||
const DEFAULT_EFFORT_VALUE = 'default';
|
||||
|
||||
const FALLBACK_EFFORT_VALUES: Partial<Record<LLMProvider, string[]>> = {
|
||||
claude: ['low', 'medium', 'high', 'xhigh', 'max'],
|
||||
codex: ['low', 'medium', 'high', 'xhigh'],
|
||||
};
|
||||
|
||||
/**
|
||||
* Fallback permission-mode matrix used only until the backend capability
|
||||
* matrix (`GET /api/providers/capabilities`) has loaded. The backend is the
|
||||
@@ -87,6 +95,12 @@ export function useChatProviderState({ selectedSession, selectedProject }: UseCh
|
||||
const [codexModel, setCodexModel] = useState<string>(() => {
|
||||
return localStorage.getItem('codex-model') || FALLBACK_DEFAULT_MODEL.codex;
|
||||
});
|
||||
const [claudeEffort, setClaudeEffort] = useState<string>(() => {
|
||||
return localStorage.getItem('claude-effort') || DEFAULT_EFFORT_VALUE;
|
||||
});
|
||||
const [codexEffort, setCodexEffort] = useState<string>(() => {
|
||||
return localStorage.getItem('codex-effort') || DEFAULT_EFFORT_VALUE;
|
||||
});
|
||||
const [geminiModel, setGeminiModel] = useState<string>(() => {
|
||||
return localStorage.getItem('gemini-model') || FALLBACK_DEFAULT_MODEL.gemini;
|
||||
});
|
||||
@@ -145,6 +159,19 @@ export function useChatProviderState({ selectedSession, selectedProject }: UseCh
|
||||
localStorage.setItem('opencode-model', model);
|
||||
}, []);
|
||||
|
||||
const setStoredProviderEffort = useCallback((targetProvider: LLMProvider, effort: string) => {
|
||||
if (targetProvider === 'claude') {
|
||||
setClaudeEffort(effort);
|
||||
localStorage.setItem('claude-effort', effort);
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetProvider === 'codex') {
|
||||
setCodexEffort(effort);
|
||||
localStorage.setItem('codex-effort', effort);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadProviderModels = useCallback(async (options: { bypassCache?: boolean } = {}) => {
|
||||
const providers: LLMProvider[] = ['claude', 'cursor', 'codex', 'gemini', 'opencode'];
|
||||
const requestId = providerModelsRequestIdRef.current + 1;
|
||||
@@ -259,6 +286,53 @@ export function useChatProviderState({ selectedSession, selectedProject }: UseCh
|
||||
return def.DEFAULT;
|
||||
};
|
||||
|
||||
const getModelOption = useCallback((
|
||||
targetProvider: LLMProvider,
|
||||
model: string,
|
||||
): ProviderModelOption | null => {
|
||||
const definition = providerModelCatalog[targetProvider];
|
||||
if (!definition) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return definition.OPTIONS.find((option) => option.value === model) ?? null;
|
||||
}, [providerModelCatalog]);
|
||||
|
||||
const getAllowedEffortValues = useCallback((
|
||||
targetProvider: LLMProvider,
|
||||
model: string,
|
||||
): string[] => {
|
||||
const option = getModelOption(targetProvider, model);
|
||||
return option?.effort?.values.map((value) => value.value) ?? FALLBACK_EFFORT_VALUES[targetProvider] ?? [];
|
||||
}, [getModelOption]);
|
||||
|
||||
const reconcileStoredEffort = useCallback((
|
||||
targetProvider: LLMProvider,
|
||||
model: string,
|
||||
currentEffort: string,
|
||||
): string => {
|
||||
const allowedValues = getAllowedEffortValues(targetProvider, model);
|
||||
if (allowedValues.length === 0) {
|
||||
return DEFAULT_EFFORT_VALUE;
|
||||
}
|
||||
|
||||
const storageKey = `${targetProvider}-effort`;
|
||||
const storedEffort = localStorage.getItem(storageKey);
|
||||
if (storedEffort === DEFAULT_EFFORT_VALUE || storedEffort === null) {
|
||||
return DEFAULT_EFFORT_VALUE;
|
||||
}
|
||||
|
||||
if (allowedValues.includes(storedEffort)) {
|
||||
return storedEffort;
|
||||
}
|
||||
|
||||
if (allowedValues.includes(currentEffort)) {
|
||||
return currentEffort;
|
||||
}
|
||||
|
||||
return DEFAULT_EFFORT_VALUE;
|
||||
}, [getAllowedEffortValues]);
|
||||
|
||||
useEffect(() => {
|
||||
const claude = providerModelCatalog.claude;
|
||||
if (claude) {
|
||||
@@ -272,6 +346,16 @@ export function useChatProviderState({ selectedSession, selectedProject }: UseCh
|
||||
}
|
||||
}, [providerModelCatalog.claude, claudeModel]);
|
||||
|
||||
useEffect(() => {
|
||||
const next = reconcileStoredEffort('claude', claudeModel, claudeEffort);
|
||||
if (next !== claudeEffort) {
|
||||
setClaudeEffort(next);
|
||||
}
|
||||
if ((localStorage.getItem('claude-effort') || DEFAULT_EFFORT_VALUE) !== next) {
|
||||
localStorage.setItem('claude-effort', next);
|
||||
}
|
||||
}, [claudeEffort, claudeModel, reconcileStoredEffort]);
|
||||
|
||||
useEffect(() => {
|
||||
const cursor = providerModelCatalog.cursor;
|
||||
if (cursor) {
|
||||
@@ -298,6 +382,16 @@ export function useChatProviderState({ selectedSession, selectedProject }: UseCh
|
||||
}
|
||||
}, [providerModelCatalog.codex, codexModel]);
|
||||
|
||||
useEffect(() => {
|
||||
const next = reconcileStoredEffort('codex', codexModel, codexEffort);
|
||||
if (next !== codexEffort) {
|
||||
setCodexEffort(next);
|
||||
}
|
||||
if ((localStorage.getItem('codex-effort') || DEFAULT_EFFORT_VALUE) !== next) {
|
||||
localStorage.setItem('codex-effort', next);
|
||||
}
|
||||
}, [codexEffort, codexModel, reconcileStoredEffort]);
|
||||
|
||||
useEffect(() => {
|
||||
const gemini = providerModelCatalog.gemini;
|
||||
if (gemini) {
|
||||
@@ -430,6 +524,10 @@ export function useChatProviderState({ selectedSession, selectedProject }: UseCh
|
||||
setClaudeModel,
|
||||
codexModel,
|
||||
setCodexModel,
|
||||
claudeEffort,
|
||||
setClaudeEffort,
|
||||
codexEffort,
|
||||
setCodexEffort,
|
||||
geminiModel,
|
||||
setGeminiModel,
|
||||
opencodeModel,
|
||||
@@ -445,5 +543,6 @@ export function useChatProviderState({ selectedSession, selectedProject }: UseCh
|
||||
providerModelsRefreshing,
|
||||
hardRefreshProviderModels: () => loadProviderModels({ bypassCache: true }),
|
||||
selectProviderModel,
|
||||
setStoredProviderEffort,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user