Merge remote-tracking branch 'origin/main' into feat/debug-logging-refactor

This commit is contained in:
tanzhenxin 2026-02-05 20:23:48 +08:00
commit 4abec5c055
331 changed files with 19546 additions and 7771 deletions

View file

@ -45,14 +45,16 @@ export const Composer = () => {
<Box flexDirection="column" marginTop={1}>
{!uiState.embeddedShellFocused && (
<LoadingIndicator
// Hide loading phrases when enableLoadingPhrases is explicitly false.
// Using === false ensures phrases show by default when undefined.
thought={
uiState.streamingState === StreamingState.WaitingForConfirmation ||
config.getAccessibility()?.disableLoadingPhrases
config.getAccessibility()?.enableLoadingPhrases === false
? undefined
: uiState.thought
}
currentLoadingPhrase={
config.getAccessibility()?.disableLoadingPhrases
config.getAccessibility()?.enableLoadingPhrases === false
? undefined
: uiState.currentLoadingPhrase
}

View file

@ -47,30 +47,35 @@ const renderComponent = (
setValue: vi.fn(),
} as unknown as LoadedSettings;
const mockConfig = contextValue
? ({
// --- Functions used by ModelDialog ---
getModel: vi.fn(() => MAINLINE_CODER),
setModel: vi.fn().mockResolvedValue(undefined),
switchModel: vi.fn().mockResolvedValue(undefined),
getAuthType: vi.fn(() => 'qwen-oauth'),
const mockConfig = {
// --- Functions used by ModelDialog ---
getModel: vi.fn(() => MAINLINE_CODER),
setModel: vi.fn().mockResolvedValue(undefined),
switchModel: vi.fn().mockResolvedValue(undefined),
getAuthType: vi.fn(() => 'qwen-oauth'),
getAllConfiguredModels: vi.fn(() =>
AVAILABLE_MODELS_QWEN.map((m) => ({
id: m.id,
label: m.label,
description: m.description || '',
authType: AuthType.QWEN_OAUTH,
})),
),
// --- Functions used by ClearcutLogger ---
getUsageStatisticsEnabled: vi.fn(() => true),
getSessionId: vi.fn(() => 'mock-session-id'),
getDebugMode: vi.fn(() => false),
getContentGeneratorConfig: vi.fn(() => ({
authType: AuthType.QWEN_OAUTH,
model: MAINLINE_CODER,
})),
getUseSmartEdit: vi.fn(() => false),
getUseModelRouter: vi.fn(() => false),
getProxy: vi.fn(() => undefined),
// --- Functions used by ClearcutLogger ---
getUsageStatisticsEnabled: vi.fn(() => true),
getSessionId: vi.fn(() => 'mock-session-id'),
getDebugMode: vi.fn(() => false),
getContentGeneratorConfig: vi.fn(() => ({
authType: AuthType.QWEN_OAUTH,
model: MAINLINE_CODER,
})),
getUseModelRouter: vi.fn(() => false),
getProxy: vi.fn(() => undefined),
// --- Spread test-specific overrides ---
...contextValue,
} as unknown as Config)
: undefined;
// --- Spread test-specific overrides ---
...(contextValue ?? {}),
} as unknown as Config;
const renderResult = render(
<SettingsContext.Provider value={mockSettings}>
@ -176,10 +181,6 @@ describe('<ModelDialog />', () => {
AuthType.QWEN_OAUTH,
MAINLINE_CODER,
undefined,
{
reason: 'user_manual',
context: 'Model switched via /model dialog',
},
);
expect(mockSettings.setValue).toHaveBeenCalledWith(
SettingScope.User,
@ -236,10 +237,6 @@ describe('<ModelDialog />', () => {
AuthType.QWEN_OAUTH,
MAINLINE_CODER,
{ requireCachedCredentials: true },
{
reason: 'user_manual',
context: 'AuthType+model switched via /model dialog',
},
);
expect(mockSettings.setValue).toHaveBeenCalledWith(
SettingScope.User,
@ -308,6 +305,14 @@ describe('<ModelDialog />', () => {
{
getModel: mockGetModel,
getAuthType: mockGetAuthType,
getAllConfiguredModels: vi.fn(() =>
AVAILABLE_MODELS_QWEN.map((m) => ({
id: m.id,
label: m.label,
description: m.description || '',
authType: AuthType.QWEN_OAUTH,
})),
),
} as unknown as Config
}
>
@ -322,6 +327,14 @@ describe('<ModelDialog />', () => {
const newMockConfig = {
getModel: mockGetModel,
getAuthType: mockGetAuthType,
getAllConfiguredModels: vi.fn(() =>
AVAILABLE_MODELS_QWEN.map((m) => ({
id: m.id,
label: m.label,
description: m.description || '',
authType: AuthType.QWEN_OAUTH,
})),
),
} as unknown as Config;
rerender(

View file

@ -11,6 +11,7 @@ import {
AuthType,
ModelSlashCommandEvent,
logModelSlashCommand,
type AvailableModel as CoreAvailableModel,
type ContentGeneratorConfig,
type ContentGeneratorConfigSource,
type ContentGeneratorConfigSources,
@ -19,12 +20,9 @@ import { useKeypress } from '../hooks/useKeypress.js';
import { theme } from '../semantic-colors.js';
import { DescriptiveRadioButtonSelect } from './shared/DescriptiveRadioButtonSelect.js';
import { ConfigContext } from '../contexts/ConfigContext.js';
import { UIStateContext } from '../contexts/UIStateContext.js';
import { UIStateContext, type UIState } from '../contexts/UIStateContext.js';
import { useSettings } from '../contexts/SettingsContext.js';
import {
getAvailableModelsForAuthType,
MAINLINE_CODER,
} from '../models/availableModels.js';
import { MAINLINE_CODER } from '../models/availableModels.js';
import { getPersistScopeForModelSelection } from '../../config/modelProvidersScope.js';
import { t } from '../../i18n/index.js';
@ -105,6 +103,46 @@ function persistAuthTypeSelection(
settings.setValue(scope, 'security.auth.selectedType', authType);
}
interface HandleModelSwitchSuccessParams {
settings: ReturnType<typeof useSettings>;
uiState: UIState | null;
after: ContentGeneratorConfig | undefined;
effectiveAuthType: AuthType | undefined;
effectiveModelId: string;
isRuntime: boolean;
}
function handleModelSwitchSuccess({
settings,
uiState,
after,
effectiveAuthType,
effectiveModelId,
isRuntime,
}: HandleModelSwitchSuccessParams): void {
persistModelSelection(settings, effectiveModelId);
if (effectiveAuthType) {
persistAuthTypeSelection(settings, effectiveAuthType);
}
const baseUrl = after?.baseUrl ?? t('(default)');
const maskedKey = maskApiKey(after?.apiKey);
uiState?.historyManager.addItem(
{
type: 'info',
text:
`authType: ${effectiveAuthType ?? '(none)'}` +
`\n` +
`Using ${isRuntime ? 'runtime ' : ''}model: ${effectiveModelId}` +
`\n` +
`Base URL: ${baseUrl}` +
`\n` +
`API key: ${maskedKey}`,
},
Date.now(),
);
}
function ConfigRow({
label,
value,
@ -154,13 +192,21 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
const sources = readSourcesFromConfig(config);
const availableModelEntries = useMemo(() => {
const allAuthTypes = Object.values(AuthType) as AuthType[];
const modelsByAuthType = allAuthTypes
.map((t) => ({
authType: t,
models: getAvailableModelsForAuthType(t, config ?? undefined),
}))
.filter((x) => x.models.length > 0);
const allModels = config ? config.getAllConfiguredModels() : [];
// Separate runtime models from registry models
const runtimeModels = allModels.filter((m) => m.isRuntimeModel);
const registryModels = allModels.filter((m) => !m.isRuntimeModel);
// Group registry models by authType
const modelsByAuthTypeMap = new Map<AuthType, CoreAvailableModel[]>();
for (const model of registryModels) {
const authType = model.authType;
if (!modelsByAuthTypeMap.has(authType)) {
modelsByAuthTypeMap.set(authType, []);
}
modelsByAuthTypeMap.get(authType)!.push(model);
}
// Fixed order: qwen-oauth first, then others in a stable order
const authTypeOrder: AuthType[] = [
@ -171,44 +217,91 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
AuthType.USE_VERTEX_AI,
];
// Filter to only include authTypes that have models
const availableAuthTypes = new Set(modelsByAuthType.map((x) => x.authType));
// Filter to only include authTypes that have registry models and maintain order
const availableAuthTypes = new Set(modelsByAuthTypeMap.keys());
const orderedAuthTypes = authTypeOrder.filter((t) =>
availableAuthTypes.has(t),
);
return orderedAuthTypes.flatMap((t) => {
const models =
modelsByAuthType.find((x) => x.authType === t)?.models ?? [];
return models.map((m) => ({ authType: t, model: m }));
});
// Build ordered list: runtime models first, then registry models grouped by authType
const result: Array<{
authType: AuthType;
model: CoreAvailableModel;
isRuntime?: boolean;
snapshotId?: string;
}> = [];
// Add all runtime models first
for (const runtimeModel of runtimeModels) {
result.push({
authType: runtimeModel.authType,
model: runtimeModel,
isRuntime: true,
snapshotId: runtimeModel.runtimeSnapshotId,
});
}
// Add registry models grouped by authType
for (const t of orderedAuthTypes) {
for (const model of modelsByAuthTypeMap.get(t) ?? []) {
result.push({ authType: t, model, isRuntime: false });
}
}
return result;
}, [config]);
const MODEL_OPTIONS = useMemo(
() =>
availableModelEntries.map(({ authType: t2, model }) => {
const value = `${t2}::${model.id}`;
const title = (
<Text>
<Text bold color={theme.text.accent}>
[{t2}]
availableModelEntries.map(
({ authType: t2, model, isRuntime, snapshotId }) => {
// Runtime models use snapshotId directly (format: $runtime|${authType}|${modelId})
const value =
isRuntime && snapshotId ? snapshotId : `${t2}::${model.id}`;
const title = (
<Text>
<Text
bold
color={isRuntime ? theme.status.warning : theme.text.accent}
>
[{t2}]
</Text>
<Text>{` ${model.label}`}</Text>
{isRuntime && (
<Text color={theme.status.warning}> (Runtime)</Text>
)}
</Text>
<Text>{` ${model.label}`}</Text>
</Text>
);
const description = model.description || '';
return {
value,
title,
description,
key: value,
};
}),
);
// Include runtime indicator in description
let description = model.description || '';
if (isRuntime) {
description = description
? `${description} (Runtime)`
: 'Runtime model';
}
return {
value,
title,
description,
key: value,
};
},
),
[availableModelEntries],
);
const preferredModelId = config?.getModel() || MAINLINE_CODER;
const preferredKey = authType ? `${authType}::${preferredModelId}` : '';
// Check if current model is a runtime model
// Runtime snapshot ID is already in $runtime|${authType}|${modelId} format
const activeRuntimeSnapshot = config?.getActiveRuntimeModelSnapshot?.();
const preferredKey = activeRuntimeSnapshot
? activeRuntimeSnapshot.id
: authType
? `${authType}::${preferredModelId}`
: '';
useKeypress(
(key) => {
@ -228,67 +321,81 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
const handleSelect = useCallback(
async (selected: string) => {
// Clear any previous error
setErrorMessage(null);
const sep = '::';
const idx = selected.indexOf(sep);
const selectedAuthType = (
idx >= 0 ? selected.slice(0, idx) : authType
) as AuthType;
const modelId = idx >= 0 ? selected.slice(idx + sep.length) : selected;
let after: ContentGeneratorConfig | undefined;
let effectiveAuthType: AuthType | undefined;
let effectiveModelId = selected;
let isRuntime = false;
if (config) {
try {
await config.switchModel(
selectedAuthType,
modelId,
selectedAuthType !== authType &&
selectedAuthType === AuthType.QWEN_OAUTH
? { requireCachedCredentials: true }
: undefined,
{
reason: 'user_manual',
context:
selectedAuthType === authType
? 'Model switched via /model dialog'
: 'AuthType+model switched via /model dialog',
},
);
} catch (e) {
const baseErrorMessage = e instanceof Error ? e.message : String(e);
setErrorMessage(
`Failed to switch model to '${modelId}'.\n\n${baseErrorMessage}`,
);
return;
if (!config) {
onClose();
return;
}
try {
// Determine if this is a runtime model selection
// Runtime model format: $runtime|${authType}|${modelId}
isRuntime = selected.startsWith('$runtime|');
let selectedAuthType: AuthType;
let modelId: string;
if (isRuntime) {
// For runtime models, extract authType from the snapshot ID
// Format: $runtime|${authType}|${modelId}
const parts = selected.split('|');
if (parts.length >= 2 && parts[0] === '$runtime') {
selectedAuthType = parts[1] as AuthType;
} else {
selectedAuthType = authType as AuthType;
}
modelId = selected; // Pass the full snapshot ID to switchModel
} else {
const sep = '::';
const idx = selected.indexOf(sep);
selectedAuthType = (
idx >= 0 ? selected.slice(0, idx) : authType
) as AuthType;
modelId = idx >= 0 ? selected.slice(idx + sep.length) : selected;
}
const event = new ModelSlashCommandEvent(modelId);
logModelSlashCommand(config, event);
const after = config.getContentGeneratorConfig?.() as
await config.switchModel(
selectedAuthType,
modelId,
selectedAuthType !== authType &&
selectedAuthType === AuthType.QWEN_OAUTH
? { requireCachedCredentials: true }
: undefined,
);
if (!isRuntime) {
const event = new ModelSlashCommandEvent(modelId);
logModelSlashCommand(config, event);
}
after = config.getContentGeneratorConfig?.() as
| ContentGeneratorConfig
| undefined;
const effectiveAuthType =
after?.authType ?? selectedAuthType ?? authType;
const effectiveModelId = after?.model ?? modelId;
persistModelSelection(settings, effectiveModelId);
persistAuthTypeSelection(settings, effectiveAuthType);
const baseUrl = after?.baseUrl ?? t('(default)');
const maskedKey = maskApiKey(after?.apiKey);
uiState?.historyManager.addItem(
{
type: 'info',
text:
`authType: ${effectiveAuthType}\n` +
`Using model: ${effectiveModelId}\n` +
`Base URL: ${baseUrl}\n` +
`API key: ${maskedKey}`,
},
Date.now(),
);
effectiveAuthType = after?.authType ?? selectedAuthType ?? authType;
effectiveModelId = after?.model ?? modelId;
} catch (e) {
const baseErrorMessage = e instanceof Error ? e.message : String(e);
const errorPrefix = isRuntime
? 'Failed to switch to runtime model.'
: `Failed to switch model to '${effectiveModelId ?? selected}'.`;
setErrorMessage(`${errorPrefix}\n\n${baseErrorMessage}`);
return;
}
handleModelSwitchSuccess({
settings,
uiState,
after,
effectiveAuthType,
effectiveModelId,
isRuntime,
});
onClose();
},
[authType, config, onClose, settings, uiState, setErrorMessage],