mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-30 12:40:44 +00:00
resolve comment
This commit is contained in:
commit
e4e21bb6b7
93 changed files with 3360 additions and 1604 deletions
|
|
@ -107,6 +107,10 @@ class GeminiAgent {
|
|||
audio: true,
|
||||
embeddedContext: true,
|
||||
},
|
||||
sessionCapabilities: {
|
||||
list: {},
|
||||
resume: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -153,10 +157,14 @@ class GeminiAgent {
|
|||
|
||||
const session = await this.createAndStoreSession(config);
|
||||
const availableModels = this.buildAvailableModels(config);
|
||||
const modesData = this.buildModesData(config);
|
||||
const configOptions = this.buildConfigOptions(config);
|
||||
|
||||
return {
|
||||
sessionId: session.getId(),
|
||||
models: availableModels,
|
||||
modes: modesData,
|
||||
configOptions,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -239,25 +247,31 @@ class GeminiAgent {
|
|||
async listSessions(
|
||||
params: acp.ListSessionsRequest,
|
||||
): Promise<acp.ListSessionsResponse> {
|
||||
const sessionService = new SessionService(params.cwd);
|
||||
const cwd = params.cwd || process.cwd();
|
||||
const sessionService = new SessionService(cwd);
|
||||
const result = await sessionService.listSessions({
|
||||
cursor: params.cursor,
|
||||
size: params.size,
|
||||
});
|
||||
|
||||
const sessions = result.items.map((item) => ({
|
||||
cwd: item.cwd,
|
||||
filePath: item.filePath,
|
||||
gitBranch: item.gitBranch,
|
||||
messageCount: item.messageCount,
|
||||
mtime: item.mtime,
|
||||
prompt: item.prompt,
|
||||
sessionId: item.sessionId,
|
||||
startTime: item.startTime,
|
||||
title: item.prompt || '(session)',
|
||||
updatedAt: new Date(item.mtime).toISOString(),
|
||||
}));
|
||||
|
||||
return {
|
||||
items: result.items.map((item) => ({
|
||||
sessionId: item.sessionId,
|
||||
cwd: item.cwd,
|
||||
startTime: item.startTime,
|
||||
mtime: item.mtime,
|
||||
prompt: item.prompt,
|
||||
gitBranch: item.gitBranch,
|
||||
filePath: item.filePath,
|
||||
messageCount: item.messageCount,
|
||||
})),
|
||||
nextCursor: result.nextCursor,
|
||||
hasMore: result.hasMore,
|
||||
items: sessions,
|
||||
nextCursor: result.nextCursor,
|
||||
sessions,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -449,6 +463,70 @@ class GeminiAgent {
|
|||
};
|
||||
}
|
||||
|
||||
private buildModesData(config: Config): acp.ModesData {
|
||||
const currentApprovalMode = config.getApprovalMode();
|
||||
|
||||
const availableModes = APPROVAL_MODES.map((mode) => ({
|
||||
id: mode as ApprovalModeValue,
|
||||
name: APPROVAL_MODE_INFO[mode].name,
|
||||
description: APPROVAL_MODE_INFO[mode].description,
|
||||
}));
|
||||
|
||||
return {
|
||||
currentModeId: currentApprovalMode as ApprovalModeValue,
|
||||
availableModes,
|
||||
};
|
||||
}
|
||||
|
||||
private buildConfigOptions(config: Config): acp.ConfigOption[] {
|
||||
const currentApprovalMode = config.getApprovalMode();
|
||||
const currentModelId = this.formatCurrentModelId(
|
||||
config.getModel() || this.config.getModel() || '',
|
||||
config.getAuthType(),
|
||||
);
|
||||
|
||||
const modeOptions = APPROVAL_MODES.map((mode) => ({
|
||||
value: mode,
|
||||
name: APPROVAL_MODE_INFO[mode].name,
|
||||
description: APPROVAL_MODE_INFO[mode].description,
|
||||
}));
|
||||
|
||||
const allConfiguredModels = config.getAllConfiguredModels();
|
||||
const modelOptions = allConfiguredModels.map((model) => {
|
||||
const effectiveModelId =
|
||||
model.isRuntimeModel && model.runtimeSnapshotId
|
||||
? model.runtimeSnapshotId
|
||||
: model.id;
|
||||
|
||||
return {
|
||||
value: formatAcpModelId(effectiveModelId, model.authType),
|
||||
name: model.label,
|
||||
description: model.description ?? '',
|
||||
};
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'mode',
|
||||
name: 'Mode',
|
||||
description: 'Session permission mode',
|
||||
category: 'mode',
|
||||
type: 'select',
|
||||
currentValue: currentApprovalMode,
|
||||
options: modeOptions,
|
||||
},
|
||||
{
|
||||
id: 'model',
|
||||
name: 'Model',
|
||||
description: 'AI model to use',
|
||||
category: 'model',
|
||||
type: 'select',
|
||||
currentValue: currentModelId,
|
||||
options: modelOptions,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private formatCurrentModelId(
|
||||
baseModelId: string,
|
||||
authType?: AuthType,
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export type CancelNotification = z.infer<typeof cancelNotificationSchema>;
|
|||
|
||||
export type AuthenticateRequest = z.infer<typeof authenticateRequestSchema>;
|
||||
|
||||
export type NewSessionResponse = z.infer<typeof newSessionResponseSchema>;
|
||||
// Note: NewSessionResponse type is defined later after newSessionResponseSchema
|
||||
|
||||
export type LoadSessionResponse = z.infer<typeof loadSessionResponseSchema>;
|
||||
|
||||
|
|
@ -285,33 +285,33 @@ export const sessionModelStateSchema = z.object({
|
|||
currentModelId: modelIdSchema,
|
||||
});
|
||||
|
||||
export const newSessionResponseSchema = z.object({
|
||||
sessionId: z.string(),
|
||||
models: sessionModelStateSchema,
|
||||
});
|
||||
// Note: newSessionResponseSchema is defined later in the file after modesDataSchema
|
||||
|
||||
export const loadSessionResponseSchema = z.null();
|
||||
|
||||
export const sessionListItemSchema = z.object({
|
||||
cwd: z.string(),
|
||||
filePath: z.string(),
|
||||
filePath: z.string().optional(),
|
||||
gitBranch: z.string().optional(),
|
||||
messageCount: z.number(),
|
||||
mtime: z.number(),
|
||||
prompt: z.string(),
|
||||
messageCount: z.number().optional(),
|
||||
mtime: z.number().optional(),
|
||||
prompt: z.string().optional(),
|
||||
sessionId: z.string(),
|
||||
startTime: z.string(),
|
||||
startTime: z.string().optional(),
|
||||
title: z.string(),
|
||||
updatedAt: z.string(),
|
||||
});
|
||||
|
||||
export const listSessionsResponseSchema = z.object({
|
||||
hasMore: z.boolean(),
|
||||
items: z.array(sessionListItemSchema),
|
||||
hasMore: z.boolean().optional(),
|
||||
items: z.array(sessionListItemSchema).optional(),
|
||||
nextCursor: z.number().optional(),
|
||||
sessions: z.array(sessionListItemSchema),
|
||||
});
|
||||
|
||||
export const listSessionsRequestSchema = z.object({
|
||||
cursor: z.number().optional(),
|
||||
cwd: z.string(),
|
||||
cwd: z.string().optional(),
|
||||
size: z.number().optional(),
|
||||
});
|
||||
|
||||
|
|
@ -405,6 +405,12 @@ export const promptCapabilitiesSchema = z.object({
|
|||
export const agentCapabilitiesSchema = z.object({
|
||||
loadSession: z.boolean().optional(),
|
||||
promptCapabilities: promptCapabilitiesSchema.optional(),
|
||||
sessionCapabilities: z
|
||||
.object({
|
||||
list: z.object({}).optional(),
|
||||
resume: z.object({}).optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const authMethodSchema = z.object({
|
||||
|
|
@ -451,6 +457,34 @@ export const modesDataSchema = z.object({
|
|||
availableModes: z.array(modeInfoSchema),
|
||||
});
|
||||
|
||||
export const configOptionSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
category: z.string(),
|
||||
type: z.string(),
|
||||
currentValue: z.string(),
|
||||
options: z.array(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export type ConfigOption = z.infer<typeof configOptionSchema>;
|
||||
|
||||
// newSessionResponseSchema includes modes and configOptions for ACP/Zed integration
|
||||
export const newSessionResponseSchema = z.object({
|
||||
sessionId: z.string(),
|
||||
models: sessionModelStateSchema,
|
||||
modes: modesDataSchema,
|
||||
configOptions: z.array(configOptionSchema),
|
||||
});
|
||||
|
||||
export type NewSessionResponse = z.infer<typeof newSessionResponseSchema>;
|
||||
|
||||
export const agentInfoSchema = z.object({
|
||||
name: z.string(),
|
||||
title: z.string(),
|
||||
|
|
|
|||
|
|
@ -698,14 +698,21 @@ export async function loadCliConfig(
|
|||
}
|
||||
|
||||
// Automatically load output-language.md if it exists
|
||||
let outputLanguageFilePath: string | undefined = path.join(
|
||||
const projectStorage = new Storage(cwd);
|
||||
const projectOutputLanguagePath = path.join(
|
||||
projectStorage.getQwenDir(),
|
||||
'output-language.md',
|
||||
);
|
||||
const globalOutputLanguagePath = path.join(
|
||||
Storage.getGlobalQwenDir(),
|
||||
'output-language.md',
|
||||
);
|
||||
if (fs.existsSync(outputLanguageFilePath)) {
|
||||
// output-language.md found - will be added to context files
|
||||
} else {
|
||||
outputLanguageFilePath = undefined;
|
||||
|
||||
let outputLanguageFilePath: string | undefined;
|
||||
if (fs.existsSync(projectOutputLanguagePath)) {
|
||||
outputLanguageFilePath = projectOutputLanguagePath;
|
||||
} else if (fs.existsSync(globalOutputLanguagePath)) {
|
||||
outputLanguageFilePath = globalOutputLanguagePath;
|
||||
}
|
||||
|
||||
const fileService = new FileDiscoveryService(cwd);
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ export enum Command {
|
|||
QUIT = 'quit',
|
||||
EXIT = 'exit',
|
||||
SHOW_MORE_LINES = 'showMoreLines',
|
||||
RETRY_LAST = 'retryLast',
|
||||
|
||||
// Shell commands
|
||||
REVERSE_SEARCH = 'reverseSearch',
|
||||
|
|
@ -170,6 +171,7 @@ export const defaultKeyBindings: KeyBindingConfig = {
|
|||
[Command.QUIT]: [{ key: 'c', ctrl: true }],
|
||||
[Command.EXIT]: [{ key: 'd', ctrl: true }],
|
||||
[Command.SHOW_MORE_LINES]: [{ key: 's', ctrl: true }],
|
||||
[Command.RETRY_LAST]: [{ key: 'y', ctrl: true }],
|
||||
|
||||
// Shell commands
|
||||
[Command.REVERSE_SEARCH]: [{ key: 'r', ctrl: true }],
|
||||
|
|
|
|||
|
|
@ -251,15 +251,9 @@ export function getCodingPlanConfig(region: CodingPlanRegion) {
|
|||
region === CodingPlanRegion.CHINA
|
||||
? 'https://coding.dashscope.aliyuncs.com/v1'
|
||||
: 'https://coding-intl.dashscope.aliyuncs.com/v1';
|
||||
const regionName =
|
||||
region === CodingPlanRegion.CHINA
|
||||
? 'Coding Plan (Bailian, China)'
|
||||
: 'Coding Plan (Bailian, Global/Intl)';
|
||||
|
||||
return {
|
||||
template,
|
||||
baseUrl,
|
||||
regionName,
|
||||
version: computeCodingPlanVersion(template),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,6 +157,7 @@ export default {
|
|||
'Enter to confirm, Esc to cancel': 'Enter zum Bestätigen, Esc zum Abbrechen',
|
||||
'Enter to select, ↑↓ to navigate, Esc to go back':
|
||||
'Enter zum Auswählen, ↑↓ zum Navigieren, Esc zum Zurückgehen',
|
||||
'Enter to submit, Esc to go back': 'Enter zum Absenden, Esc zum Zurückgehen',
|
||||
'Invalid step: {{step}}': 'Ungültiger Schritt: {{step}}',
|
||||
'No subagents found.': 'Keine Unteragenten gefunden.',
|
||||
"Use '/agents create' to create your first subagent.":
|
||||
|
|
@ -944,18 +945,22 @@ export default {
|
|||
// Dialogs - Auth
|
||||
// ============================================================================
|
||||
'Get started': 'Loslegen',
|
||||
'How would you like to authenticate for this project?':
|
||||
'Wie möchten Sie sich für dieses Projekt authentifizieren?',
|
||||
'Select Authentication Method': 'Authentifizierungsmethode auswählen',
|
||||
'OpenAI API key is required to use OpenAI authentication.':
|
||||
'OpenAI API-Schlüssel ist für die OpenAI-Authentifizierung erforderlich.',
|
||||
'You must select an auth method to proceed. Press Ctrl+C again to exit.':
|
||||
'Sie müssen eine Authentifizierungsmethode wählen, um fortzufahren. Drücken Sie erneut Strg+C zum Beenden.',
|
||||
'(Use Enter to Set Auth)': '(Enter zum Festlegen der Authentifizierung)',
|
||||
'Terms of Services and Privacy Notice for Qwen Code':
|
||||
'Nutzungsbedingungen und Datenschutzhinweis für Qwen Code',
|
||||
'Terms of Services and Privacy Notice':
|
||||
'Nutzungsbedingungen und Datenschutzhinweis',
|
||||
'Qwen OAuth': 'Qwen OAuth',
|
||||
'Free \u00B7 Up to 1,000 requests/day \u00B7 Qwen latest models':
|
||||
'Kostenlos \u00B7 Bis zu 1.000 Anfragen/Tag \u00B7 Qwen neueste Modelle',
|
||||
'Login with QwenChat account to use daily free quota.':
|
||||
'Melden Sie sich mit Ihrem QwenChat-Konto an, um das tägliche kostenlose Kontingent zu nutzen.',
|
||||
'Paid \u00B7 Up to 6,000 requests/5 hrs \u00B7 All Alibaba Cloud Coding Plan Models':
|
||||
'Kostenpflichtig \u00B7 Bis zu 6.000 Anfragen/5 Std. \u00B7 Alle Alibaba Cloud Coding Plan Modelle',
|
||||
'Alibaba Cloud Coding Plan': 'Alibaba Cloud Coding Plan',
|
||||
'Bring your own API key': 'Eigenen API-Schlüssel verwenden',
|
||||
'API-KEY': 'API-KEY',
|
||||
'Use coding plan credentials or your own api-keys/providers.':
|
||||
'Verwenden Sie Coding Plan-Anmeldedaten oder Ihre eigenen API-Schlüssel/Anbieter.',
|
||||
|
|
@ -985,6 +990,8 @@ export default {
|
|||
'Warten auf Qwen OAuth-Authentifizierung...',
|
||||
'Note: Your existing API key in settings.json will not be cleared when using Qwen OAuth. You can switch back to OpenAI authentication later if needed.':
|
||||
'Hinweis: Ihr bestehender API-Schlüssel in settings.json wird bei Verwendung von Qwen OAuth nicht gelöscht. Sie können später bei Bedarf zur OpenAI-Authentifizierung zurückwechseln.',
|
||||
'Note: Your existing API key will not be cleared when using Qwen OAuth.':
|
||||
'Hinweis: Ihr bestehender API-Schlüssel wird bei Verwendung von Qwen OAuth nicht gelöscht.',
|
||||
'Authentication timed out. Please try again.':
|
||||
'Authentifizierung abgelaufen. Bitte versuchen Sie es erneut.',
|
||||
'Waiting for auth... (Press ESC or CTRL+C to cancel)':
|
||||
|
|
@ -1034,6 +1041,17 @@ export default {
|
|||
'(default)': '(Standard)',
|
||||
'(set)': '(gesetzt)',
|
||||
'(not set)': '(nicht gesetzt)',
|
||||
Modality: 'Modalität',
|
||||
'Context Window': 'Kontextfenster',
|
||||
text: 'Text',
|
||||
'text-only': 'nur Text',
|
||||
image: 'Bild',
|
||||
pdf: 'PDF',
|
||||
audio: 'Audio',
|
||||
video: 'Video',
|
||||
'not set': 'nicht gesetzt',
|
||||
none: 'keine',
|
||||
unknown: 'unbekannt',
|
||||
"Failed to switch model to '{{modelId}}'.\n\n{{error}}":
|
||||
"Modell konnte nicht auf '{{modelId}}' umgestellt werden.\n\n{{error}}",
|
||||
'Qwen 3.5 Plus — efficient hybrid model with leading coding performance':
|
||||
|
|
@ -1380,38 +1398,43 @@ export default {
|
|||
'Erweiterungsseite wird im Browser geöffnet: {{url}}',
|
||||
'Failed to open browser. Check out the extensions gallery at {{url}}':
|
||||
'Browser konnte nicht geöffnet werden. Besuchen Sie die Erweiterungsgalerie unter {{url}}',
|
||||
'Use /compress when the conversation gets long to summarize history and free up context.':
|
||||
'Verwenden Sie /compress, wenn die Unterhaltung lang wird, um den Verlauf zusammenzufassen und Kontext freizugeben.',
|
||||
'Start a fresh idea with /clear or /new; the previous session stays available in history.':
|
||||
'Starten Sie eine neue Idee mit /clear oder /new; die vorherige Sitzung bleibt im Verlauf verfügbar.',
|
||||
'Use /bug to submit issues to the maintainers when something goes off.':
|
||||
'Verwenden Sie /bug, um Probleme an die Betreuer zu melden, wenn etwas schiefgeht.',
|
||||
'Switch auth type quickly with /auth.':
|
||||
'Wechseln Sie den Authentifizierungstyp schnell mit /auth.',
|
||||
'You can run any shell commands from Qwen Code using ! (e.g. !ls).':
|
||||
'Sie können beliebige Shell-Befehle in Qwen Code mit ! ausführen (z. B. !ls).',
|
||||
'Type / to open the command popup; Tab autocompletes slash commands and saved prompts.':
|
||||
'Geben Sie / ein, um das Befehlsmenü zu öffnen; Tab vervollständigt Slash-Befehle und gespeicherte Prompts.',
|
||||
'You can resume a previous conversation by running qwen --continue or qwen --resume.':
|
||||
'Sie können eine frühere Unterhaltung mit qwen --continue oder qwen --resume fortsetzen.',
|
||||
'You can switch permission mode quickly with Shift+Tab or /approval-mode.':
|
||||
'Sie können den Berechtigungsmodus schnell mit Shift+Tab oder /approval-mode wechseln.',
|
||||
'You can switch permission mode quickly with Tab or /approval-mode.':
|
||||
'Sie können den Berechtigungsmodus schnell mit Tab oder /approval-mode wechseln.',
|
||||
'Try /insight to generate personalized insights from your chat history.':
|
||||
'Probieren Sie /insight, um personalisierte Erkenntnisse aus Ihrem Chatverlauf zu erstellen.',
|
||||
|
||||
// ============================================================================
|
||||
// Custom API-KEY Configuration
|
||||
// Custom API Key Configuration
|
||||
// ============================================================================
|
||||
'For advanced users who want to configure models manually.':
|
||||
'Für fortgeschrittene Benutzer, die Modelle manuell konfigurieren möchten.',
|
||||
'Please configure your models in settings.json:':
|
||||
'Bitte konfigurieren Sie Ihre Modelle in settings.json:',
|
||||
'Set API key via environment variable (e.g., OPENAI_API_KEY)':
|
||||
'API-Schlüssel über Umgebungsvariable setzen (z.B. OPENAI_API_KEY)',
|
||||
"Add model configuration to modelProviders['openai'] (or other auth types)":
|
||||
"Modellkonfiguration zu modelProviders['openai'] (oder anderen Authentifizierungstypen) hinzufügen",
|
||||
'Each provider needs: id, envKey (required), plus optional baseUrl, generationConfig':
|
||||
'Jeder Anbieter benötigt: id, envKey (erforderlich), plus optionale baseUrl, generationConfig',
|
||||
'Use /model command to select your preferred model from the configured list':
|
||||
'Verwenden Sie den /model-Befehl, um Ihr bevorzugtes Modell aus der konfigurierten Liste auszuwählen',
|
||||
'Supported auth types: openai, anthropic, gemini, vertex-ai, etc.':
|
||||
'Unterstützte Authentifizierungstypen: openai, anthropic, gemini, vertex-ai, usw.',
|
||||
'You can configure your API key and models in settings.json':
|
||||
'Sie können Ihren API-Schlüssel und Modelle in settings.json konfigurieren',
|
||||
'Refer to the documentation for setup instructions':
|
||||
'Einrichtungsanweisungen finden Sie in der Dokumentation',
|
||||
|
||||
// ============================================================================
|
||||
// Coding Plan Authentication
|
||||
// ============================================================================
|
||||
'Please enter your API key:': 'Bitte geben Sie Ihren API-Schlüssel ein:',
|
||||
'API key cannot be empty.': 'API-Schlüssel darf nicht leer sein.',
|
||||
'You can get your exclusive Coding Plan API-KEY here:':
|
||||
'Hier können Sie Ihren exklusiven Coding Plan API-KEY erhalten:',
|
||||
'New model configurations are available for Bailian Coding Plan. Update now?':
|
||||
'Neue Modellkonfigurationen sind für Bailian Coding Plan verfügbar. Jetzt aktualisieren?',
|
||||
'You can get your Coding Plan API key here':
|
||||
'Sie können Ihren Coding-Plan-API-Schlüssel hier erhalten',
|
||||
'New model configurations are available for Alibaba Cloud Coding Plan. Update now?':
|
||||
'Neue Modellkonfigurationen sind für Alibaba Cloud Coding Plan verfügbar. Jetzt aktualisieren?',
|
||||
'Coding Plan configuration updated successfully. New models are now available.':
|
||||
'Coding Plan-Konfiguration erfolgreich aktualisiert. Neue Modelle sind jetzt verfügbar.',
|
||||
'Coding Plan API key not found. Please re-authenticate with Coding Plan.':
|
||||
|
|
@ -1422,32 +1445,16 @@ export default {
|
|||
// ============================================================================
|
||||
// Auth Dialog - View Titles and Labels
|
||||
// ============================================================================
|
||||
'Coding Plan': 'Coding Plan',
|
||||
'Coding Plan (Bailian, China)': 'Coding Plan (Bailian, China)',
|
||||
'Coding Plan (Bailian, Global/Intl)': 'Coding Plan (Bailian, Global/Intl)',
|
||||
"Paste your api key of Bailian Coding Plan and you're all set!":
|
||||
'Fügen Sie Ihren Bailian Coding Plan API-Schlüssel ein und Sie sind bereit!',
|
||||
"Paste your api key of Coding Plan (Bailian, Global/Intl) and you're all set!":
|
||||
'Fügen Sie Ihren Coding Plan (Bailian, Global/Intl) API-Schlüssel ein und Sie sind bereit!',
|
||||
Custom: 'Benutzerdefiniert',
|
||||
'More instructions about configuring `modelProviders` manually.':
|
||||
'Weitere Anweisungen zur manuellen Konfiguration von `modelProviders`.',
|
||||
'Select API-KEY configuration mode:':
|
||||
'API-KEY-Konfigurationsmodus auswählen:',
|
||||
'(Press Escape to go back)': '(Escape drücken zum Zurückgehen)',
|
||||
'(Press Enter to submit, Escape to cancel)':
|
||||
'(Enter zum Absenden, Escape zum Abbrechen)',
|
||||
'More instructions please check:': 'Weitere Anweisungen finden Sie unter:',
|
||||
'Select Region for Coding Plan': 'Region für Coding Plan auswählen',
|
||||
'Choose based on where your account is registered':
|
||||
'Wählen Sie basierend auf dem Registrierungsort Ihres Kontos',
|
||||
'Enter Coding Plan API Key': 'Coding-Plan-API-Schlüssel eingeben',
|
||||
|
||||
// ============================================================================
|
||||
// Coding Plan International Updates
|
||||
// ============================================================================
|
||||
'New model configurations are available for {{region}}. Update now?':
|
||||
'Neue Modellkonfigurationen sind für {{region}} verfügbar. Jetzt aktualisieren?',
|
||||
'New model configurations are available for Bailian Coding Plan (China). Update now?':
|
||||
'Neue Modellkonfigurationen sind für Bailian Coding Plan (China) verfügbar. Jetzt aktualisieren?',
|
||||
'New model configurations are available for Coding Plan (Bailian, Global/Intl). Update now?':
|
||||
'Neue Modellkonfigurationen sind für Coding Plan (Bailian, Global/Intl) verfügbar. Jetzt aktualisieren?',
|
||||
'{{region}} configuration updated successfully. Model switched to "{{model}}".':
|
||||
'{{region}}-Konfiguration erfolgreich aktualisiert. Modell auf "{{model}}" umgeschaltet.',
|
||||
'Authenticated successfully with {{region}}. API key and model configs saved to settings.json (backed up).':
|
||||
|
|
|
|||
|
|
@ -178,6 +178,7 @@ export default {
|
|||
'Enter to confirm, Esc to cancel': 'Enter to confirm, Esc to cancel',
|
||||
'Enter to select, ↑↓ to navigate, Esc to go back':
|
||||
'Enter to select, ↑↓ to navigate, Esc to go back',
|
||||
'Enter to submit, Esc to go back': 'Enter to submit, Esc to go back',
|
||||
'Invalid step: {{step}}': 'Invalid step: {{step}}',
|
||||
'No subagents found.': 'No subagents found.',
|
||||
"Use '/agents create' to create your first subagent.":
|
||||
|
|
@ -935,18 +936,22 @@ export default {
|
|||
// Dialogs - Auth
|
||||
// ============================================================================
|
||||
'Get started': 'Get started',
|
||||
'How would you like to authenticate for this project?':
|
||||
'How would you like to authenticate for this project?',
|
||||
'Select Authentication Method': 'Select Authentication Method',
|
||||
'OpenAI API key is required to use OpenAI authentication.':
|
||||
'OpenAI API key is required to use OpenAI authentication.',
|
||||
'You must select an auth method to proceed. Press Ctrl+C again to exit.':
|
||||
'You must select an auth method to proceed. Press Ctrl+C again to exit.',
|
||||
'(Use Enter to Set Auth)': '(Use Enter to Set Auth)',
|
||||
'Terms of Services and Privacy Notice for Qwen Code':
|
||||
'Terms of Services and Privacy Notice for Qwen Code',
|
||||
'Terms of Services and Privacy Notice':
|
||||
'Terms of Services and Privacy Notice',
|
||||
'Qwen OAuth': 'Qwen OAuth',
|
||||
'Free \u00B7 Up to 1,000 requests/day \u00B7 Qwen latest models':
|
||||
'Free \u00B7 Up to 1,000 requests/day \u00B7 Qwen latest models',
|
||||
'Login with QwenChat account to use daily free quota.':
|
||||
'Login with QwenChat account to use daily free quota.',
|
||||
'Paid \u00B7 Up to 6,000 requests/5 hrs \u00B7 All Alibaba Cloud Coding Plan Models':
|
||||
'Paid \u00B7 Up to 6,000 requests/5 hrs \u00B7 All Alibaba Cloud Coding Plan Models',
|
||||
'Alibaba Cloud Coding Plan': 'Alibaba Cloud Coding Plan',
|
||||
'Bring your own API key': 'Bring your own API key',
|
||||
'API-KEY': 'API-KEY',
|
||||
'Use coding plan credentials or your own api-keys/providers.':
|
||||
'Use coding plan credentials or your own api-keys/providers.',
|
||||
|
|
@ -974,6 +979,8 @@ export default {
|
|||
'Waiting for Qwen OAuth authentication...',
|
||||
'Note: Your existing API key in settings.json will not be cleared when using Qwen OAuth. You can switch back to OpenAI authentication later if needed.':
|
||||
'Note: Your existing API key in settings.json will not be cleared when using Qwen OAuth. You can switch back to OpenAI authentication later if needed.',
|
||||
'Note: Your existing API key will not be cleared when using Qwen OAuth.':
|
||||
'Note: Your existing API key will not be cleared when using Qwen OAuth.',
|
||||
'Authentication timed out. Please try again.':
|
||||
'Authentication timed out. Please try again.',
|
||||
'Waiting for auth... (Press ESC or CTRL+C to cancel)':
|
||||
|
|
@ -1021,6 +1028,17 @@ export default {
|
|||
'(default)': '(default)',
|
||||
'(set)': '(set)',
|
||||
'(not set)': '(not set)',
|
||||
Modality: 'Modality',
|
||||
'Context Window': 'Context Window',
|
||||
text: 'text',
|
||||
'text-only': 'text-only',
|
||||
image: 'image',
|
||||
pdf: 'pdf',
|
||||
audio: 'audio',
|
||||
video: 'video',
|
||||
'not set': 'not set',
|
||||
none: 'none',
|
||||
unknown: 'unknown',
|
||||
"Failed to switch model to '{{modelId}}'.\n\n{{error}}":
|
||||
"Failed to switch model to '{{modelId}}'.\n\n{{error}}",
|
||||
'Qwen 3.5 Plus — efficient hybrid model with leading coding performance':
|
||||
|
|
@ -1115,6 +1133,8 @@ export default {
|
|||
'You can switch permission mode quickly with Shift+Tab or /approval-mode.',
|
||||
'You can switch permission mode quickly with Tab or /approval-mode.':
|
||||
'You can switch permission mode quickly with Tab or /approval-mode.',
|
||||
'Try /insight to generate personalized insights from your chat history.':
|
||||
'Try /insight to generate personalized insights from your chat history.',
|
||||
|
||||
// ============================================================================
|
||||
// Exit Screen / Stats
|
||||
|
|
@ -1382,18 +1402,20 @@ export default {
|
|||
'Rate limit error: {{reason}}': 'Rate limit error: {{reason}}',
|
||||
'Retrying in {{seconds}} seconds… (attempt {{attempt}}/{{maxRetries}})':
|
||||
'Retrying in {{seconds}} seconds… (attempt {{attempt}}/{{maxRetries}})',
|
||||
'Press Ctrl+Y to retry': 'Press Ctrl+Y to retry',
|
||||
'No failed request to retry.': 'No failed request to retry.',
|
||||
'to retry last request': 'to retry last request',
|
||||
|
||||
// ============================================================================
|
||||
// Coding Plan Authentication
|
||||
// ============================================================================
|
||||
'Please enter your API key:': 'Please enter your API key:',
|
||||
'API key cannot be empty.': 'API key cannot be empty.',
|
||||
'You can get your exclusive Coding Plan API-KEY here:':
|
||||
'You can get your exclusive Coding Plan API-KEY here:',
|
||||
'You can get your Coding Plan API key here':
|
||||
'You can get your Coding Plan API key here',
|
||||
'API key is stored in settings.env. You can migrate it to a .env file for better security.':
|
||||
'API key is stored in settings.env. You can migrate it to a .env file for better security.',
|
||||
'New model configurations are available for Bailian Coding Plan. Update now?':
|
||||
'New model configurations are available for Bailian Coding Plan. Update now?',
|
||||
'New model configurations are available for Alibaba Cloud Coding Plan. Update now?':
|
||||
'New model configurations are available for Alibaba Cloud Coding Plan. Update now?',
|
||||
'Coding Plan configuration updated successfully. New models are now available.':
|
||||
'Coding Plan configuration updated successfully. New models are now available.',
|
||||
'Coding Plan API key not found. Please re-authenticate with Coding Plan.':
|
||||
|
|
@ -1402,51 +1424,26 @@ export default {
|
|||
'Failed to update Coding Plan configuration: {{message}}',
|
||||
|
||||
// ============================================================================
|
||||
// Custom API-KEY Configuration
|
||||
// Custom API Key Configuration
|
||||
// ============================================================================
|
||||
'For advanced users who want to configure models manually.':
|
||||
'For advanced users who want to configure models manually.',
|
||||
'Please configure your models in settings.json:':
|
||||
'Please configure your models in settings.json:',
|
||||
'Set API key via environment variable (e.g., OPENAI_API_KEY)':
|
||||
'Set API key via environment variable (e.g., OPENAI_API_KEY)',
|
||||
"Add model configuration to modelProviders['openai'] (or other auth types)":
|
||||
"Add model configuration to modelProviders['openai'] (or other auth types)",
|
||||
'Each provider needs: id, envKey (required), plus optional baseUrl, generationConfig':
|
||||
'Each provider needs: id, envKey (required), plus optional baseUrl, generationConfig',
|
||||
'Use /model command to select your preferred model from the configured list':
|
||||
'Use /model command to select your preferred model from the configured list',
|
||||
'Supported auth types: openai, anthropic, gemini, vertex-ai, etc.':
|
||||
'Supported auth types: openai, anthropic, gemini, vertex-ai, etc.',
|
||||
'More instructions please check:': 'More instructions please check:',
|
||||
'You can configure your API key and models in settings.json':
|
||||
'You can configure your API key and models in settings.json',
|
||||
'Refer to the documentation for setup instructions':
|
||||
'Refer to the documentation for setup instructions',
|
||||
|
||||
// ============================================================================
|
||||
// Auth Dialog - View Titles and Labels
|
||||
// ============================================================================
|
||||
'Coding Plan': 'Coding Plan',
|
||||
'Coding Plan (Bailian, China)': 'Coding Plan (Bailian, China)',
|
||||
'Coding Plan (Bailian, Global/Intl)': 'Coding Plan (Bailian, Global/Intl)',
|
||||
"Paste your api key of Bailian Coding Plan and you're all set!":
|
||||
"Paste your api key of Bailian Coding Plan and you're all set!",
|
||||
"Paste your api key of Coding Plan (Bailian, Global/Intl) and you're all set!":
|
||||
"Paste your api key of Coding Plan (Bailian, Global/Intl) and you're all set!",
|
||||
Custom: 'Custom',
|
||||
'More instructions about configuring `modelProviders` manually.':
|
||||
'More instructions about configuring `modelProviders` manually.',
|
||||
'Select API-KEY configuration mode:': 'Select API-KEY configuration mode:',
|
||||
'(Press Escape to go back)': '(Press Escape to go back)',
|
||||
'(Press Enter to submit, Escape to cancel)':
|
||||
'(Press Enter to submit, Escape to cancel)',
|
||||
'Select Region for Coding Plan': 'Select Region for Coding Plan',
|
||||
'Choose based on where your account is registered':
|
||||
'Choose based on where your account is registered',
|
||||
'Enter Coding Plan API Key': 'Enter Coding Plan API Key',
|
||||
|
||||
// ============================================================================
|
||||
// Coding Plan International Updates
|
||||
// ============================================================================
|
||||
'New model configurations are available for {{region}}. Update now?':
|
||||
'New model configurations are available for {{region}}. Update now?',
|
||||
'New model configurations are available for Bailian Coding Plan (China). Update now?':
|
||||
'New model configurations are available for Bailian Coding Plan (China). Update now?',
|
||||
'New model configurations are available for Coding Plan (Bailian, Global/Intl). Update now?':
|
||||
'New model configurations are available for Coding Plan (Bailian, Global/Intl). Update now?',
|
||||
'{{region}} configuration updated successfully. Model switched to "{{model}}".':
|
||||
'{{region}} configuration updated successfully. Model switched to "{{model}}".',
|
||||
'Authenticated successfully with {{region}}. API key and model configs saved to settings.json (backed up).':
|
||||
|
|
|
|||
|
|
@ -142,6 +142,7 @@ export default {
|
|||
'Enter to confirm, Esc to cancel': 'Enter で確定、Esc でキャンセル',
|
||||
'Enter to select, ↑↓ to navigate, Esc to go back':
|
||||
'Enter で選択、↑↓ で移動、Esc で戻る',
|
||||
'Enter to submit, Esc to go back': 'Enter で送信、Esc で戻る',
|
||||
'Invalid step: {{step}}': '無効なステップ: {{step}}',
|
||||
'No subagents found.': 'サブエージェントが見つかりません',
|
||||
"Use '/agents create' to create your first subagent.":
|
||||
|
|
@ -671,18 +672,21 @@ export default {
|
|||
'🎯 Overall Goal:': '🎯 全体目標:',
|
||||
// Dialogs - Auth
|
||||
'Get started': '始める',
|
||||
'How would you like to authenticate for this project?':
|
||||
'このプロジェクトの認証方法を選択してください:',
|
||||
'Select Authentication Method': '認証方法を選択',
|
||||
'OpenAI API key is required to use OpenAI authentication.':
|
||||
'OpenAI認証を使用するには OpenAI APIキーが必要です',
|
||||
'You must select an auth method to proceed. Press Ctrl+C again to exit.':
|
||||
'続行するには認証方法を選択してください。Ctrl+C をもう一度押すと終了します',
|
||||
'(Use Enter to Set Auth)': '(Enter で認証を設定)',
|
||||
'Terms of Services and Privacy Notice for Qwen Code':
|
||||
'Qwen Code の利用規約とプライバシー通知',
|
||||
'Terms of Services and Privacy Notice': '利用規約とプライバシー通知',
|
||||
'Qwen OAuth': 'Qwen OAuth',
|
||||
'Free \u00B7 Up to 1,000 requests/day \u00B7 Qwen latest models':
|
||||
'無料 \u00B7 1日最大1,000リクエスト \u00B7 Qwen最新モデル',
|
||||
'Login with QwenChat account to use daily free quota.':
|
||||
'QwenChatアカウントでログインして、毎日の無料クォータをご利用ください。',
|
||||
'Paid \u00B7 Up to 6,000 requests/5 hrs \u00B7 All Alibaba Cloud Coding Plan Models':
|
||||
'有料 \u00B7 5時間最大6,000リクエスト \u00B7 すべての Alibaba Cloud Coding Plan モデル',
|
||||
'Alibaba Cloud Coding Plan': 'Alibaba Cloud Coding Plan',
|
||||
'Bring your own API key': '自分のAPIキーを使用',
|
||||
'API-KEY': 'API-KEY',
|
||||
'Use coding plan credentials or your own api-keys/providers.':
|
||||
'Coding Planの認証情報またはご自身のAPIキー/プロバイダーをご利用ください。',
|
||||
|
|
@ -710,6 +714,8 @@ export default {
|
|||
'Waiting for Qwen OAuth authentication...': 'Qwen OAuth認証を待っています...',
|
||||
'Note: Your existing API key in settings.json will not be cleared when using Qwen OAuth. You can switch back to OpenAI authentication later if needed.':
|
||||
'注: Qwen OAuthを使用しても、settings.json内の既存のAPIキーはクリアされません。必要に応じて後でOpenAI認証に切り替えることができます',
|
||||
'Note: Your existing API key will not be cleared when using Qwen OAuth.':
|
||||
'注: Qwen OAuthを使用しても、既存のAPIキーはクリアされません。',
|
||||
'Authentication timed out. Please try again.':
|
||||
'認証がタイムアウトしました。再度お試しください',
|
||||
'Waiting for auth... (Press ESC or CTRL+C to cancel)':
|
||||
|
|
@ -731,6 +737,17 @@ export default {
|
|||
// Dialogs - Model
|
||||
'Select Model': 'モデルを選択',
|
||||
'(Press Esc to close)': '(Esc で閉じる)',
|
||||
Modality: 'モダリティ',
|
||||
'Context Window': 'コンテキストウィンドウ',
|
||||
text: 'テキスト',
|
||||
'text-only': 'テキストのみ',
|
||||
image: '画像',
|
||||
pdf: 'PDF',
|
||||
audio: '音声',
|
||||
video: '動画',
|
||||
'not set': '未設定',
|
||||
none: 'なし',
|
||||
unknown: '不明',
|
||||
'Qwen 3.5 Plus — efficient hybrid model with leading coding performance':
|
||||
'Qwen 3.5 Plus — 効率的なハイブリッドモデル、業界トップクラスのコーディング性能',
|
||||
'The latest Qwen Vision model from Alibaba Cloud ModelStudio (version: qwen3-vl-plus-2025-09-23)':
|
||||
|
|
@ -783,6 +800,27 @@ export default {
|
|||
"Starting OAuth authentication for MCP server '{{name}}'...":
|
||||
"MCPサーバー '{{name}}' のOAuth認証を開始中...",
|
||||
// Startup Tips
|
||||
'Tips:': 'ヒント:',
|
||||
'Use /compress when the conversation gets long to summarize history and free up context.':
|
||||
'会話が長くなったら /compress で履歴を要約し、コンテキストを解放できます。',
|
||||
'Start a fresh idea with /clear or /new; the previous session stays available in history.':
|
||||
'/clear または /new で新しいアイデアを始められます。前のセッションは履歴に残ります。',
|
||||
'Use /bug to submit issues to the maintainers when something goes off.':
|
||||
'問題が発生したら /bug でメンテナーに報告できます。',
|
||||
'Switch auth type quickly with /auth.':
|
||||
'/auth で認証タイプをすばやく切り替えられます。',
|
||||
'You can run any shell commands from Qwen Code using ! (e.g. !ls).':
|
||||
'Qwen Code から ! を使って任意のシェルコマンドを実行できます(例: !ls)。',
|
||||
'Type / to open the command popup; Tab autocompletes slash commands and saved prompts.':
|
||||
'/ を入力してコマンドポップアップを開きます。Tab でスラッシュコマンドと保存済みプロンプトを補完できます。',
|
||||
'You can resume a previous conversation by running qwen --continue or qwen --resume.':
|
||||
'qwen --continue または qwen --resume で前の会話を再開できます。',
|
||||
'You can switch permission mode quickly with Shift+Tab or /approval-mode.':
|
||||
'Shift+Tab または /approval-mode で権限モードをすばやく切り替えられます。',
|
||||
'You can switch permission mode quickly with Tab or /approval-mode.':
|
||||
'Tab または /approval-mode で権限モードをすばやく切り替えられます。',
|
||||
'Try /insight to generate personalized insights from your chat history.':
|
||||
'/insight でチャット履歴からパーソナライズされたインサイトを生成できます。',
|
||||
'Tips for getting started:': '始めるためのヒント:',
|
||||
'1. Ask questions, edit files, or run commands.':
|
||||
'1. 質問したり、ファイルを編集したり、コマンドを実行したりできます',
|
||||
|
|
@ -891,32 +929,19 @@ export default {
|
|||
],
|
||||
|
||||
// ============================================================================
|
||||
// Custom API-KEY Configuration
|
||||
// Custom API Key Configuration
|
||||
// ============================================================================
|
||||
'For advanced users who want to configure models manually.':
|
||||
'モデルを手動で設定したい上級ユーザー向け。',
|
||||
'Please configure your models in settings.json:':
|
||||
'settings.json でモデルを設定してください:',
|
||||
'Set API key via environment variable (e.g., OPENAI_API_KEY)':
|
||||
'環境変数を使用して API キーを設定してください(例:OPENAI_API_KEY)',
|
||||
"Add model configuration to modelProviders['openai'] (or other auth types)":
|
||||
"modelProviders['openai'](または他の認証タイプ)にモデル設定を追加してください",
|
||||
'Each provider needs: id, envKey (required), plus optional baseUrl, generationConfig':
|
||||
'各プロバイダーには:id、envKey(必須)、およびオプションの baseUrl、generationConfig が必要です',
|
||||
'Use /model command to select your preferred model from the configured list':
|
||||
'/model コマンドを使用して、設定済みリストからお好みのモデルを選択してください',
|
||||
'Supported auth types: openai, anthropic, gemini, vertex-ai, etc.':
|
||||
'サポートされている認証タイプ:openai、anthropic、gemini、vertex-ai など',
|
||||
'You can configure your API key and models in settings.json':
|
||||
'settings.json で API キーとモデルを設定できます',
|
||||
'Refer to the documentation for setup instructions':
|
||||
'セットアップ手順はドキュメントを参照してください',
|
||||
|
||||
// ============================================================================
|
||||
// Coding Plan Authentication
|
||||
// ============================================================================
|
||||
'Please enter your API key:': 'APIキーを入力してください:',
|
||||
'API key cannot be empty.': 'APIキーは空にできません。',
|
||||
'You can get your exclusive Coding Plan API-KEY here:':
|
||||
'Coding Plan の API-KEY はこちらで取得できます:',
|
||||
'New model configurations are available for Bailian Coding Plan. Update now?':
|
||||
'Bailian Coding Plan の新しいモデル設定が利用可能です。今すぐ更新しますか?',
|
||||
'You can get your Coding Plan API key here':
|
||||
'Coding Plan APIキーはこちらで取得できます',
|
||||
'Coding Plan configuration updated successfully. New models are now available.':
|
||||
'Coding Plan の設定が正常に更新されました。新しいモデルが利用可能になりました。',
|
||||
'Coding Plan API key not found. Please re-authenticate with Coding Plan.':
|
||||
|
|
@ -927,32 +952,16 @@ export default {
|
|||
// ============================================================================
|
||||
// Auth Dialog - View Titles and Labels
|
||||
// ============================================================================
|
||||
'Coding Plan': 'Coding Plan',
|
||||
'Coding Plan (Bailian, China)': 'Coding Plan (Bailian, 中国)',
|
||||
'Coding Plan (Bailian, Global/Intl)':
|
||||
'Coding Plan (Bailian, グローバル/国際)',
|
||||
"Paste your api key of Bailian Coding Plan and you're all set!":
|
||||
'Bailian Coding PlanのAPIキーを貼り付けるだけで準備完了です!',
|
||||
"Paste your api key of Coding Plan (Bailian, Global/Intl) and you're all set!":
|
||||
'Coding Plan (Bailian, グローバル/国際) のAPIキーを貼り付けるだけで準備完了です!',
|
||||
Custom: 'カスタム',
|
||||
'More instructions about configuring `modelProviders` manually.':
|
||||
'`modelProviders`を手動で設定する方法の詳細はこちら。',
|
||||
'Select API-KEY configuration mode:': 'API-KEY設定モードを選択してください:',
|
||||
'(Press Escape to go back)': '(Escapeキーで戻る)',
|
||||
'(Press Enter to submit, Escape to cancel)':
|
||||
'(Enterで送信、Escapeでキャンセル)',
|
||||
'More instructions please check:': '詳細な手順はこちらをご確認ください:',
|
||||
'Select Region for Coding Plan': 'Coding Planのリージョンを選択',
|
||||
'Choose based on where your account is registered':
|
||||
'アカウントの登録先に応じて選択してください',
|
||||
'Enter Coding Plan API Key': 'Coding Plan APIキーを入力',
|
||||
|
||||
// ============================================================================
|
||||
// Coding Plan International Updates
|
||||
// ============================================================================
|
||||
'New model configurations are available for {{region}}. Update now?':
|
||||
'{{region}} の新しいモデル設定が利用可能です。今すぐ更新しますか?',
|
||||
'New model configurations are available for Bailian Coding Plan (China). Update now?':
|
||||
'Bailian Coding Plan (中国) の新しいモデル設定が利用可能です。今すぐ更新しますか?',
|
||||
'New model configurations are available for Coding Plan (Bailian, Global/Intl). Update now?':
|
||||
'Coding Plan (Bailian, グローバル/国際) の新しいモデル設定が利用可能です。今すぐ更新しますか?',
|
||||
'{{region}} configuration updated successfully. Model switched to "{{model}}".':
|
||||
'{{region}} の設定が正常に更新されました。モデルが "{{model}}" に切り替わりました。',
|
||||
'Authenticated successfully with {{region}}. API key and model configs saved to settings.json (backed up).':
|
||||
|
|
|
|||
|
|
@ -173,6 +173,7 @@ export default {
|
|||
'Enter to confirm, Esc to cancel': 'Enter para confirmar, Esc para cancelar',
|
||||
'Enter to select, ↑↓ to navigate, Esc to go back':
|
||||
'Enter para selecionar, ↑↓ para navegar, Esc para voltar',
|
||||
'Enter to submit, Esc to go back': 'Enter para enviar, Esc para voltar',
|
||||
'Invalid step: {{step}}': 'Etapa inválida: {{step}}',
|
||||
'No subagents found.': 'Nenhum subagente encontrado.',
|
||||
"Use '/agents create' to create your first subagent.":
|
||||
|
|
@ -950,18 +951,22 @@ export default {
|
|||
// Dialogs - Auth
|
||||
// ============================================================================
|
||||
'Get started': 'Começar',
|
||||
'How would you like to authenticate for this project?':
|
||||
'Como você gostaria de se autenticar para este projeto?',
|
||||
'Select Authentication Method': 'Selecionar Método de Autenticação',
|
||||
'OpenAI API key is required to use OpenAI authentication.':
|
||||
'A chave da API do OpenAI é necessária para usar a autenticação do OpenAI.',
|
||||
'You must select an auth method to proceed. Press Ctrl+C again to exit.':
|
||||
'Você deve selecionar um método de autenticação para prosseguir. Pressione Ctrl+C novamente para sair.',
|
||||
'(Use Enter to Set Auth)': '(Use Enter para Definir Autenticação)',
|
||||
'Terms of Services and Privacy Notice for Qwen Code':
|
||||
'Termos de Serviço e Aviso de Privacidade do Qwen Code',
|
||||
'Terms of Services and Privacy Notice':
|
||||
'Termos de Serviço e Aviso de Privacidade',
|
||||
'Qwen OAuth': 'Qwen OAuth',
|
||||
'Free \u00B7 Up to 1,000 requests/day \u00B7 Qwen latest models':
|
||||
'Gratuito \u00B7 Até 1.000 solicitações/dia \u00B7 Modelos Qwen mais recentes',
|
||||
'Login with QwenChat account to use daily free quota.':
|
||||
'Faça login com sua conta QwenChat para usar a cota gratuita diária.',
|
||||
'Paid \u00B7 Up to 6,000 requests/5 hrs \u00B7 All Alibaba Cloud Coding Plan Models':
|
||||
'Pago \u00B7 Até 6.000 solicitações/5 hrs \u00B7 Todos os modelos Alibaba Cloud Coding Plan',
|
||||
'Alibaba Cloud Coding Plan': 'Alibaba Cloud Coding Plan',
|
||||
'Bring your own API key': 'Traga sua própria chave API',
|
||||
'API-KEY': 'API-KEY',
|
||||
'Use coding plan credentials or your own api-keys/providers.':
|
||||
'Use credenciais do Coding Plan ou suas próprias chaves API/provedores.',
|
||||
|
|
@ -989,6 +994,8 @@ export default {
|
|||
'Aguardando autenticação Qwen OAuth...',
|
||||
'Note: Your existing API key in settings.json will not be cleared when using Qwen OAuth. You can switch back to OpenAI authentication later if needed.':
|
||||
'Nota: Sua chave de API existente no settings.json não será limpa ao usar o Qwen OAuth. Você pode voltar para a autenticação do OpenAI mais tarde, se necessário.',
|
||||
'Note: Your existing API key will not be cleared when using Qwen OAuth.':
|
||||
'Nota: Sua chave de API existente não será limpa ao usar o Qwen OAuth.',
|
||||
'Authentication timed out. Please try again.':
|
||||
'A autenticação expirou. Tente novamente.',
|
||||
'Waiting for auth... (Press ESC or CTRL+C to cancel)':
|
||||
|
|
@ -1037,6 +1044,17 @@ export default {
|
|||
'(default)': '(padrão)',
|
||||
'(set)': '(definido)',
|
||||
'(not set)': '(não definido)',
|
||||
Modality: 'Modalidade',
|
||||
'Context Window': 'Janela de Contexto',
|
||||
text: 'texto',
|
||||
'text-only': 'somente texto',
|
||||
image: 'imagem',
|
||||
pdf: 'PDF',
|
||||
audio: 'áudio',
|
||||
video: 'vídeo',
|
||||
'not set': 'não definido',
|
||||
none: 'nenhum',
|
||||
unknown: 'desconhecido',
|
||||
"Failed to switch model to '{{modelId}}'.\n\n{{error}}":
|
||||
"Falha ao trocar o modelo para '{{modelId}}'.\n\n{{error}}",
|
||||
'Qwen 3.5 Plus — efficient hybrid model with leading coding performance':
|
||||
|
|
@ -1132,6 +1150,8 @@ export default {
|
|||
'Você pode retomar uma conversa anterior executando qwen --continue ou qwen --resume.',
|
||||
'You can switch permission mode quickly with Shift+Tab or /approval-mode.':
|
||||
'Você pode alternar o modo de permissão rapidamente com Shift+Tab ou /approval-mode.',
|
||||
'Try /insight to generate personalized insights from your chat history.':
|
||||
'Experimente /insight para gerar insights personalizados do seu histórico de conversas.',
|
||||
|
||||
// ============================================================================
|
||||
// Exit Screen / Stats
|
||||
|
|
@ -1394,32 +1414,21 @@ export default {
|
|||
'Falha ao abrir o navegador. Confira a galeria de extensões em {{url}}',
|
||||
|
||||
// ============================================================================
|
||||
// Custom API-KEY Configuration
|
||||
// Custom API Key Configuration
|
||||
// ============================================================================
|
||||
'For advanced users who want to configure models manually.':
|
||||
'Para usuários avançados que desejam configurar modelos manualmente.',
|
||||
'Please configure your models in settings.json:':
|
||||
'Por favor, configure seus modelos em settings.json:',
|
||||
'Set API key via environment variable (e.g., OPENAI_API_KEY)':
|
||||
'Defina a chave de API via variável de ambiente (ex: OPENAI_API_KEY)',
|
||||
"Add model configuration to modelProviders['openai'] (or other auth types)":
|
||||
"Adicione a configuração do modelo a modelProviders['openai'] (ou outros tipos de autenticação)",
|
||||
'Each provider needs: id, envKey (required), plus optional baseUrl, generationConfig':
|
||||
'Cada provedor precisa de: id, envKey (obrigatório), além de baseUrl e generationConfig opcionais',
|
||||
'Use /model command to select your preferred model from the configured list':
|
||||
'Use o comando /model para selecionar seu modelo preferido da lista configurada',
|
||||
'Supported auth types: openai, anthropic, gemini, vertex-ai, etc.':
|
||||
'Tipos de autenticação suportados: openai, anthropic, gemini, vertex-ai, etc.',
|
||||
'You can configure your API key and models in settings.json':
|
||||
'Você pode configurar sua chave de API e modelos em settings.json',
|
||||
'Refer to the documentation for setup instructions':
|
||||
'Consulte a documentação para instruções de configuração',
|
||||
|
||||
// ============================================================================
|
||||
// Coding Plan Authentication
|
||||
// ============================================================================
|
||||
'Please enter your API key:': 'Por favor, digite sua chave de API:',
|
||||
'API key cannot be empty.': 'A chave de API não pode estar vazia.',
|
||||
'You can get your exclusive Coding Plan API-KEY here:':
|
||||
'Você pode obter sua chave de API exclusiva do Coding Plan aqui:',
|
||||
'New model configurations are available for Bailian Coding Plan. Update now?':
|
||||
'Novas configurações de modelo estão disponíveis para o Bailian Coding Plan. Atualizar agora?',
|
||||
'You can get your Coding Plan API key here':
|
||||
'Você pode obter sua chave de API do Coding Plan aqui',
|
||||
'New model configurations are available for Alibaba Cloud Coding Plan. Update now?':
|
||||
'Novas configurações de modelo estão disponíveis para o Alibaba Cloud Coding Plan. Atualizar agora?',
|
||||
'Coding Plan configuration updated successfully. New models are now available.':
|
||||
'Configuração do Coding Plan atualizada com sucesso. Novos modelos agora estão disponíveis.',
|
||||
'Coding Plan API key not found. Please re-authenticate with Coding Plan.':
|
||||
|
|
@ -1430,32 +1439,16 @@ export default {
|
|||
// ============================================================================
|
||||
// Auth Dialog - View Titles and Labels
|
||||
// ============================================================================
|
||||
'Coding Plan': 'Coding Plan',
|
||||
'Coding Plan (Bailian, China)': 'Coding Plan (Bailian, China)',
|
||||
'Coding Plan (Bailian, Global/Intl)': 'Coding Plan (Bailian, Global/Intl)',
|
||||
"Paste your api key of Bailian Coding Plan and you're all set!":
|
||||
'Cole sua chave de API do Bailian Coding Plan e pronto!',
|
||||
"Paste your api key of Coding Plan (Bailian, Global/Intl) and you're all set!":
|
||||
'Cole sua chave de API do Coding Plan (Bailian, Global/Intl) e pronto!',
|
||||
Custom: 'Personalizado',
|
||||
'More instructions about configuring `modelProviders` manually.':
|
||||
'Mais instruções sobre como configurar `modelProviders` manualmente.',
|
||||
'Select API-KEY configuration mode:':
|
||||
'Selecione o modo de configuração da API-KEY:',
|
||||
'(Press Escape to go back)': '(Pressione Escape para voltar)',
|
||||
'(Press Enter to submit, Escape to cancel)':
|
||||
'(Pressione Enter para enviar, Escape para cancelar)',
|
||||
'More instructions please check:': 'Mais instruções, consulte:',
|
||||
'Select Region for Coding Plan': 'Selecionar região do Coding Plan',
|
||||
'Choose based on where your account is registered':
|
||||
'Escolha com base em onde sua conta está registrada',
|
||||
'Enter Coding Plan API Key': 'Inserir chave de API do Coding Plan',
|
||||
|
||||
// ============================================================================
|
||||
// Coding Plan International Updates
|
||||
// ============================================================================
|
||||
'New model configurations are available for {{region}}. Update now?':
|
||||
'Novas configurações de modelo estão disponíveis para o {{region}}. Atualizar agora?',
|
||||
'New model configurations are available for Bailian Coding Plan (China). Update now?':
|
||||
'Novas configurações de modelo estão disponíveis para o Bailian Coding Plan (China). Atualizar agora?',
|
||||
'New model configurations are available for Coding Plan (Bailian, Global/Intl). Update now?':
|
||||
'Novas configurações de modelo estão disponíveis para o Coding Plan (Bailian, Global/Intl). Atualizar agora?',
|
||||
'{{region}} configuration updated successfully. Model switched to "{{model}}".':
|
||||
'Configuração do {{region}} atualizada com sucesso. Modelo alterado para "{{model}}".',
|
||||
'Authenticated successfully with {{region}}. API key and model configs saved to settings.json (backed up).':
|
||||
|
|
|
|||
|
|
@ -181,6 +181,7 @@ export default {
|
|||
'Enter to confirm, Esc to cancel': 'Enter для подтверждения, Esc для отмены',
|
||||
'Enter to select, ↑↓ to navigate, Esc to go back':
|
||||
'Enter для выбора, ↑↓ для навигации, Esc для возврата',
|
||||
'Enter to submit, Esc to go back': 'Enter для отправки, Esc для возврата',
|
||||
'Invalid step: {{step}}': 'Неверный шаг: {{step}}',
|
||||
'No subagents found.': 'Подагенты не найдены.',
|
||||
"Use '/agents create' to create your first subagent.":
|
||||
|
|
@ -950,18 +951,22 @@ export default {
|
|||
// Диалоги - Авторизация
|
||||
// ============================================================================
|
||||
'Get started': 'Начать',
|
||||
'How would you like to authenticate for this project?':
|
||||
'Как вы хотите авторизоваться для этого проекта?',
|
||||
'Select Authentication Method': 'Выберите метод авторизации',
|
||||
'OpenAI API key is required to use OpenAI authentication.':
|
||||
'Для использования авторизации OpenAI требуется ключ API OpenAI.',
|
||||
'You must select an auth method to proceed. Press Ctrl+C again to exit.':
|
||||
'Вы должны выбрать метод авторизации для продолжения. Нажмите Ctrl+C снова для выхода.',
|
||||
'(Use Enter to Set Auth)': '(Enter для установки авторизации)',
|
||||
'Terms of Services and Privacy Notice for Qwen Code':
|
||||
'Условия обслуживания и уведомление о конфиденциальности для Qwen Code',
|
||||
'Terms of Services and Privacy Notice':
|
||||
'Условия обслуживания и уведомление о конфиденциальности',
|
||||
'Qwen OAuth': 'Qwen OAuth',
|
||||
'Free \u00B7 Up to 1,000 requests/day \u00B7 Qwen latest models':
|
||||
'Бесплатно \u00B7 До 1 000 запросов/день \u00B7 Новейшие модели Qwen',
|
||||
'Login with QwenChat account to use daily free quota.':
|
||||
'Войдите с помощью аккаунта QwenChat, чтобы использовать ежедневную бесплатную квоту.',
|
||||
'Paid \u00B7 Up to 6,000 requests/5 hrs \u00B7 All Alibaba Cloud Coding Plan Models':
|
||||
'Платно \u00B7 До 6 000 запросов/5 часов \u00B7 Все модели Alibaba Cloud Coding Plan',
|
||||
'Alibaba Cloud Coding Plan': 'Alibaba Cloud Coding Plan',
|
||||
'Bring your own API key': 'Используйте свой API-ключ',
|
||||
'API-KEY': 'API-KEY',
|
||||
'Use coding plan credentials or your own api-keys/providers.':
|
||||
'Используйте учетные данные Coding Plan или свои собственные API-ключи/провайдеры.',
|
||||
|
|
@ -989,6 +994,8 @@ export default {
|
|||
'Ожидание авторизации Qwen OAuth...',
|
||||
'Note: Your existing API key in settings.json will not be cleared when using Qwen OAuth. You can switch back to OpenAI authentication later if needed.':
|
||||
'Примечание: Ваш существующий ключ API в settings.json не будет удален при использовании Qwen OAuth. Вы можете переключиться обратно на авторизацию OpenAI позже при необходимости.',
|
||||
'Note: Your existing API key will not be cleared when using Qwen OAuth.':
|
||||
'Примечание: Ваш существующий ключ API не будет удален при использовании Qwen OAuth.',
|
||||
'Authentication timed out. Please try again.':
|
||||
'Время ожидания авторизации истекло. Пожалуйста, попробуйте снова.',
|
||||
'Waiting for auth... (Press ESC or CTRL+C to cancel)':
|
||||
|
|
@ -1036,6 +1043,17 @@ export default {
|
|||
'(default)': '(по умолчанию)',
|
||||
'(set)': '(установлено)',
|
||||
'(not set)': '(не задано)',
|
||||
Modality: 'Модальность',
|
||||
'Context Window': 'Контекстное окно',
|
||||
text: 'текст',
|
||||
'text-only': 'только текст',
|
||||
image: 'изображение',
|
||||
pdf: 'PDF',
|
||||
audio: 'аудио',
|
||||
video: 'видео',
|
||||
'not set': 'не задано',
|
||||
none: 'нет',
|
||||
unknown: 'неизвестно',
|
||||
"Failed to switch model to '{{modelId}}'.\n\n{{error}}":
|
||||
"Не удалось переключиться на модель '{{modelId}}'.\n\n{{error}}",
|
||||
'Qwen 3.5 Plus — efficient hybrid model with leading coding performance':
|
||||
|
|
@ -1384,38 +1402,43 @@ export default {
|
|||
'Открываем страницу расширений в браузере: {{url}}',
|
||||
'Failed to open browser. Check out the extensions gallery at {{url}}':
|
||||
'Не удалось открыть браузер. Посетите галерею расширений по адресу {{url}}',
|
||||
'Use /compress when the conversation gets long to summarize history and free up context.':
|
||||
'Используйте /compress, когда разговор становится длинным, чтобы подвести итог и освободить контекст.',
|
||||
'Start a fresh idea with /clear or /new; the previous session stays available in history.':
|
||||
'Начните новую идею с /clear или /new; предыдущая сессия останется в истории.',
|
||||
'Use /bug to submit issues to the maintainers when something goes off.':
|
||||
'Используйте /bug, чтобы сообщить о проблемах разработчикам.',
|
||||
'Switch auth type quickly with /auth.':
|
||||
'Быстро переключите тип аутентификации с помощью /auth.',
|
||||
'You can run any shell commands from Qwen Code using ! (e.g. !ls).':
|
||||
'Вы можете выполнять любые shell-команды в Qwen Code с помощью ! (например, !ls).',
|
||||
'Type / to open the command popup; Tab autocompletes slash commands and saved prompts.':
|
||||
'Введите /, чтобы открыть меню команд; Tab автодополняет слэш-команды и сохранённые промпты.',
|
||||
'You can resume a previous conversation by running qwen --continue or qwen --resume.':
|
||||
'Вы можете продолжить предыдущий разговор, запустив qwen --continue или qwen --resume.',
|
||||
'You can switch permission mode quickly with Shift+Tab or /approval-mode.':
|
||||
'Вы можете быстро переключать режим разрешений с помощью Shift+Tab или /approval-mode.',
|
||||
'You can switch permission mode quickly with Tab or /approval-mode.':
|
||||
'Вы можете быстро переключать режим разрешений с помощью Tab или /approval-mode.',
|
||||
'Try /insight to generate personalized insights from your chat history.':
|
||||
'Попробуйте /insight, чтобы получить персонализированные выводы из истории чатов.',
|
||||
|
||||
// ============================================================================
|
||||
// Custom API-KEY Configuration
|
||||
// Custom API Key Configuration
|
||||
// ============================================================================
|
||||
'For advanced users who want to configure models manually.':
|
||||
'Для продвинутых пользователей, которые хотят настраивать модели вручную.',
|
||||
'Please configure your models in settings.json:':
|
||||
'Пожалуйста, настройте ваши модели в settings.json:',
|
||||
'Set API key via environment variable (e.g., OPENAI_API_KEY)':
|
||||
'Установите ключ API через переменную окружения (например, OPENAI_API_KEY)',
|
||||
"Add model configuration to modelProviders['openai'] (or other auth types)":
|
||||
"Добавьте конфигурацию модели в modelProviders['openai'] (или другие типы аутентификации)",
|
||||
'Each provider needs: id, envKey (required), plus optional baseUrl, generationConfig':
|
||||
'Каждому провайдеру нужны: id, envKey (обязательно), а также опциональные baseUrl, generationConfig',
|
||||
'Use /model command to select your preferred model from the configured list':
|
||||
'Используйте команду /model, чтобы выбрать предпочитаемую модель из настроенного списка',
|
||||
'Supported auth types: openai, anthropic, gemini, vertex-ai, etc.':
|
||||
'Поддерживаемые типы аутентификации: openai, anthropic, gemini, vertex-ai и др.',
|
||||
'You can configure your API key and models in settings.json':
|
||||
'Вы можете настроить API-ключ и модели в settings.json',
|
||||
'Refer to the documentation for setup instructions':
|
||||
'Инструкции по настройке см. в документации',
|
||||
|
||||
// ============================================================================
|
||||
// Coding Plan Authentication
|
||||
// ============================================================================
|
||||
'Please enter your API key:': 'Пожалуйста, введите ваш API-ключ:',
|
||||
'API key cannot be empty.': 'API-ключ не может быть пустым.',
|
||||
'You can get your exclusive Coding Plan API-KEY here:':
|
||||
'Получите свой эксклюзивный API-KEY Coding Plan здесь:',
|
||||
'New model configurations are available for Bailian Coding Plan. Update now?':
|
||||
'Доступны новые конфигурации моделей для Bailian Coding Plan. Обновить сейчас?',
|
||||
'You can get your Coding Plan API key here':
|
||||
'Вы можете получить API-ключ Coding Plan здесь',
|
||||
'New model configurations are available for Alibaba Cloud Coding Plan. Update now?':
|
||||
'Доступны новые конфигурации моделей для Alibaba Cloud Coding Plan. Обновить сейчас?',
|
||||
'Coding Plan configuration updated successfully. New models are now available.':
|
||||
'Конфигурация Coding Plan успешно обновлена. Новые модели теперь доступны.',
|
||||
'Coding Plan API key not found. Please re-authenticate with Coding Plan.':
|
||||
|
|
@ -1426,32 +1449,16 @@ export default {
|
|||
// ============================================================================
|
||||
// Auth Dialog - View Titles and Labels
|
||||
// ============================================================================
|
||||
'Coding Plan': 'Coding Plan',
|
||||
'Coding Plan (Bailian, China)': 'Coding Plan (Bailian, Китай)',
|
||||
'Coding Plan (Bailian, Global/Intl)':
|
||||
'Coding Plan (Bailian, Глобальный/Международный)',
|
||||
"Paste your api key of Bailian Coding Plan and you're all set!":
|
||||
'Вставьте ваш API-ключ Bailian Coding Plan и всё готово!',
|
||||
"Paste your api key of Coding Plan (Bailian, Global/Intl) and you're all set!":
|
||||
'Вставьте ваш API-ключ Coding Plan (Bailian, Глобальный/Международный) и всё готово!',
|
||||
Custom: 'Пользовательский',
|
||||
'More instructions about configuring `modelProviders` manually.':
|
||||
'Дополнительные инструкции по ручной настройке `modelProviders`.',
|
||||
'Select API-KEY configuration mode:': 'Выберите режим конфигурации API-KEY:',
|
||||
'(Press Escape to go back)': '(Нажмите Escape для возврата)',
|
||||
'(Press Enter to submit, Escape to cancel)':
|
||||
'(Нажмите Enter для отправки, Escape для отмены)',
|
||||
'More instructions please check:': 'Дополнительные инструкции см.:',
|
||||
'Select Region for Coding Plan': 'Выберите регион Coding Plan',
|
||||
'Choose based on where your account is registered':
|
||||
'Выберите в зависимости от места регистрации вашего аккаунта',
|
||||
'Enter Coding Plan API Key': 'Введите API-ключ Coding Plan',
|
||||
|
||||
// ============================================================================
|
||||
// Coding Plan International Updates
|
||||
// ============================================================================
|
||||
'New model configurations are available for {{region}}. Update now?':
|
||||
'Доступны новые конфигурации моделей для {{region}}. Обновить сейчас?',
|
||||
'New model configurations are available for Bailian Coding Plan (China). Update now?':
|
||||
'Доступны новые конфигурации моделей для Bailian Coding Plan (Китай). Обновить сейчас?',
|
||||
'New model configurations are available for Coding Plan (Bailian, Global/Intl). Update now?':
|
||||
'Доступны новые конфигурации моделей для Coding Plan (Bailian, Глобальный/Международный). Обновить сейчас?',
|
||||
'{{region}} configuration updated successfully. Model switched to "{{model}}".':
|
||||
'Конфигурация {{region}} успешно обновлена. Модель переключена на "{{model}}".',
|
||||
'Authenticated successfully with {{region}}. API key and model configs saved to settings.json (backed up).':
|
||||
|
|
|
|||
|
|
@ -173,6 +173,7 @@ export default {
|
|||
'Enter to confirm, Esc to cancel': 'Enter 确认,Esc 取消',
|
||||
'Enter to select, ↑↓ to navigate, Esc to go back':
|
||||
'Enter 选择,↑↓ 导航,Esc 返回',
|
||||
'Enter to submit, Esc to go back': 'Enter 提交,Esc 返回',
|
||||
'Invalid step: {{step}}': '无效步骤: {{step}}',
|
||||
'No subagents found.': '未找到子智能体。',
|
||||
"Use '/agents create' to create your first subagent.":
|
||||
|
|
@ -882,18 +883,21 @@ export default {
|
|||
// Dialogs - Auth
|
||||
// ============================================================================
|
||||
'Get started': '开始使用',
|
||||
'How would you like to authenticate for this project?':
|
||||
'您希望如何为此项目进行身份验证?',
|
||||
'Select Authentication Method': '选择认证方式',
|
||||
'OpenAI API key is required to use OpenAI authentication.':
|
||||
'使用 OpenAI 认证需要 OpenAI API 密钥',
|
||||
'You must select an auth method to proceed. Press Ctrl+C again to exit.':
|
||||
'您必须选择认证方法才能继续。再次按 Ctrl+C 退出',
|
||||
'(Use Enter to Set Auth)': '(使用 Enter 设置认证)',
|
||||
'Terms of Services and Privacy Notice for Qwen Code':
|
||||
'Qwen Code 的服务条款和隐私声明',
|
||||
'Terms of Services and Privacy Notice': '服务条款和隐私声明',
|
||||
'Qwen OAuth': 'Qwen OAuth (免费)',
|
||||
'Free \u00B7 Up to 1,000 requests/day \u00B7 Qwen latest models':
|
||||
'免费 \u00B7 每天最多 1,000 次请求 \u00B7 Qwen 最新模型',
|
||||
'Login with QwenChat account to use daily free quota.':
|
||||
'使用 QwenChat 账号登录,享受每日免费额度。',
|
||||
'Paid \u00B7 Up to 6,000 requests/5 hrs \u00B7 All Alibaba Cloud Coding Plan Models':
|
||||
'付费 \u00B7 每 5 小时最多 6,000 次请求 \u00B7 支持阿里云百炼 Coding Plan 全部模型',
|
||||
'Alibaba Cloud Coding Plan': '阿里云百炼 Coding Plan',
|
||||
'Bring your own API key': '使用自己的 API 密钥',
|
||||
'Use coding plan credentials or your own api-keys/providers.':
|
||||
'使用 Coding Plan 凭证或您自己的 API 密钥/提供商。',
|
||||
OpenAI: 'OpenAI',
|
||||
|
|
@ -917,6 +921,8 @@ export default {
|
|||
'Waiting for Qwen OAuth authentication...': '正在等待 Qwen OAuth 认证...',
|
||||
'Note: Your existing API key in settings.json will not be cleared when using Qwen OAuth. You can switch back to OpenAI authentication later if needed.':
|
||||
'注意:使用 Qwen OAuth 时,settings.json 中现有的 API 密钥不会被清除。如果需要,您可以稍后切换回 OpenAI 认证。',
|
||||
'Note: Your existing API key will not be cleared when using Qwen OAuth.':
|
||||
'注意:使用 Qwen OAuth 时,现有的 API 密钥不会被清除。',
|
||||
'Authentication timed out. Please try again.': '认证超时。请重试。',
|
||||
'Waiting for auth... (Press ESC or CTRL+C to cancel)':
|
||||
'正在等待认证...(按 ESC 或 CTRL+C 取消)',
|
||||
|
|
@ -961,6 +967,17 @@ export default {
|
|||
'(default)': '(默认)',
|
||||
'(set)': '(已设置)',
|
||||
'(not set)': '(未设置)',
|
||||
Modality: '模态',
|
||||
'Context Window': '上下文窗口',
|
||||
text: '文本',
|
||||
'text-only': '纯文本',
|
||||
image: '图像',
|
||||
pdf: 'PDF',
|
||||
audio: '音频',
|
||||
video: '视频',
|
||||
'not set': '未设置',
|
||||
none: '无',
|
||||
unknown: '未知',
|
||||
"Failed to switch model to '{{modelId}}'.\n\n{{error}}":
|
||||
"无法切换到模型 '{{modelId}}'.\n\n{{error}}",
|
||||
'Qwen 3.5 Plus — efficient hybrid model with leading coding performance':
|
||||
|
|
@ -1052,6 +1069,8 @@ export default {
|
|||
'按 Shift+Tab 或输入 /approval-mode 可快速切换权限模式。',
|
||||
'You can switch permission mode quickly with Tab or /approval-mode.':
|
||||
'按 Tab 或输入 /approval-mode 可快速切换权限模式。',
|
||||
'Try /insight to generate personalized insights from your chat history.':
|
||||
'试试 /insight,从聊天记录中生成个性化洞察。',
|
||||
|
||||
// ============================================================================
|
||||
// Exit Screen / Stats
|
||||
|
|
@ -1215,18 +1234,22 @@ export default {
|
|||
'Rate limit error: {{reason}}': '触发限流:{{reason}}',
|
||||
'Retrying in {{seconds}} seconds… (attempt {{attempt}}/{{maxRetries}})':
|
||||
'将于 {{seconds}} 秒后重试…(第 {{attempt}}/{{maxRetries}} 次)',
|
||||
'Press Ctrl+Y to retry': '按 Ctrl+Y 重试。',
|
||||
'No failed request to retry.': '没有可重试的失败请求。',
|
||||
'to retry last request': '重试上一次请求',
|
||||
|
||||
// ============================================================================
|
||||
// Coding Plan Authentication
|
||||
// ============================================================================
|
||||
'Please enter your API key:': '请输入您的 API Key:',
|
||||
'API key cannot be empty.': 'API Key 不能为空。',
|
||||
'You can get your exclusive Coding Plan API-KEY here:':
|
||||
'您可以在这里获取专属的 Coding Plan API-KEY:',
|
||||
'Invalid API key. Coding Plan API keys start with "sk-sp-". Please check.':
|
||||
'无效的 API Key,Coding Plan API Key 均以 "sk-sp-" 开头,请检查',
|
||||
'You can get your Coding Plan API key here':
|
||||
'您可以在这里获取 Coding Plan API Key',
|
||||
'API key is stored in settings.env. You can migrate it to a .env file for better security.':
|
||||
'API Key 已存储在 settings.env 中。您可以将其迁移到 .env 文件以获得更好的安全性。',
|
||||
'New model configurations are available for Bailian Coding Plan. Update now?':
|
||||
'百炼 Coding Plan 有新模型配置可用。是否立即更新?',
|
||||
'New model configurations are available for Alibaba Cloud Coding Plan. Update now?':
|
||||
'阿里云百炼 Coding Plan 有新模型配置可用。是否立即更新?',
|
||||
'Coding Plan configuration updated successfully. New models are now available.':
|
||||
'Coding Plan 配置更新成功。新模型现已可用。',
|
||||
'Coding Plan API key not found. Please re-authenticate with Coding Plan.':
|
||||
|
|
@ -1235,51 +1258,25 @@ export default {
|
|||
'更新 Coding Plan 配置失败:{{message}}',
|
||||
|
||||
// ============================================================================
|
||||
// Custom API-KEY Configuration
|
||||
// Custom API Key Configuration
|
||||
// ============================================================================
|
||||
'For advanced users who want to configure models manually.':
|
||||
'适合需要手动配置模型的高级用户。',
|
||||
'Please configure your models in settings.json:':
|
||||
'请在 settings.json 中配置您的模型:',
|
||||
'Set API key via environment variable (e.g., OPENAI_API_KEY)':
|
||||
'通过环境变量设置 API Key(例如:OPENAI_API_KEY)',
|
||||
"Add model configuration to modelProviders['openai'] (or other auth types)":
|
||||
"将模型配置添加到 modelProviders['openai'](或其他认证类型)",
|
||||
'Each provider needs: id, envKey (required), plus optional baseUrl, generationConfig':
|
||||
'每个提供商需要:id、envKey(必需),以及可选的 baseUrl、generationConfig',
|
||||
'Use /model command to select your preferred model from the configured list':
|
||||
'使用 /model 命令从配置列表中选择您偏好的模型',
|
||||
'Supported auth types: openai, anthropic, gemini, vertex-ai, etc.':
|
||||
'支持的认证类型:openai、anthropic、gemini、vertex-ai 等',
|
||||
'More instructions please check:': '更多说明请查看:',
|
||||
'You can configure your API key and models in settings.json':
|
||||
'您可以在 settings.json 中配置 API Key 和模型',
|
||||
'Refer to the documentation for setup instructions': '请参考文档了解配置说明',
|
||||
|
||||
// ============================================================================
|
||||
// Auth Dialog - View Titles and Labels
|
||||
// ============================================================================
|
||||
'API-KEY': 'API-KEY',
|
||||
'Coding Plan': 'Coding Plan',
|
||||
'Coding Plan (Bailian, China)': 'Coding Plan (百炼, 中国)',
|
||||
'Coding Plan (Bailian, Global/Intl)': 'Coding Plan (百炼, 全球/国际)',
|
||||
"Paste your api key of Bailian Coding Plan and you're all set!":
|
||||
'粘贴您的百炼 Coding Plan API Key,即可完成设置!',
|
||||
"Paste your api key of Coding Plan (Bailian, Global/Intl) and you're all set!":
|
||||
'粘贴您的 Coding Plan (百炼, 全球/国际) API Key,即可完成设置!',
|
||||
Custom: '自定义',
|
||||
'More instructions about configuring `modelProviders` manually.':
|
||||
'关于手动配置 `modelProviders` 的更多说明。',
|
||||
'Select API-KEY configuration mode:': '选择 API-KEY 配置模式:',
|
||||
'(Press Escape to go back)': '(按 Escape 键返回)',
|
||||
'(Press Enter to submit, Escape to cancel)': '(按 Enter 提交,Escape 取消)',
|
||||
'Select Region for Coding Plan': '选择 Coding Plan 区域',
|
||||
'Choose based on where your account is registered':
|
||||
'请根据您的账号注册地区选择',
|
||||
'Enter Coding Plan API Key': '输入 Coding Plan API Key',
|
||||
|
||||
// ============================================================================
|
||||
// Coding Plan International Updates
|
||||
// ============================================================================
|
||||
'New model configurations are available for {{region}}. Update now?':
|
||||
'{{region}} 有新的模型配置可用。是否立即更新?',
|
||||
'New model configurations are available for Bailian Coding Plan (China). Update now?':
|
||||
'百炼 Coding Plan (中国) 有新的模型配置可用。是否立即更新?',
|
||||
'New model configurations are available for Coding Plan (Bailian, Global/Intl). Update now?':
|
||||
'Coding Plan (百炼, 全球/国际) 有新的模型配置可用。是否立即更新?',
|
||||
'{{region}} configuration updated successfully. Model switched to "{{model}}".':
|
||||
'{{region}} 配置更新成功。模型已切换至 "{{model}}"。',
|
||||
'Authenticated successfully with {{region}}. API key and model configs saved to settings.json (backed up).':
|
||||
|
|
|
|||
|
|
@ -209,6 +209,7 @@ describe('AppContainer State Management', () => {
|
|||
pendingHistoryItems: [],
|
||||
thought: null,
|
||||
cancelOngoingRequest: vi.fn(),
|
||||
retryLastPrompt: vi.fn(),
|
||||
});
|
||||
mockedUseVim.mockReturnValue({ handleInput: vi.fn() });
|
||||
mockedUseFolderTrust.mockReturnValue({
|
||||
|
|
@ -607,6 +608,7 @@ describe('AppContainer State Management', () => {
|
|||
pendingHistoryItems: [],
|
||||
thought: { subject: thoughtSubject },
|
||||
cancelOngoingRequest: vi.fn(),
|
||||
retryLastPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
// Act: Render the container
|
||||
|
|
@ -652,6 +654,7 @@ describe('AppContainer State Management', () => {
|
|||
pendingHistoryItems: [],
|
||||
thought: null,
|
||||
cancelOngoingRequest: vi.fn(),
|
||||
retryLastPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
// Act: Render the container
|
||||
|
|
@ -698,6 +701,7 @@ describe('AppContainer State Management', () => {
|
|||
pendingHistoryItems: [],
|
||||
thought: { subject: thoughtSubject },
|
||||
cancelOngoingRequest: vi.fn(),
|
||||
retryLastPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
// Act: Render the container
|
||||
|
|
@ -744,6 +748,7 @@ describe('AppContainer State Management', () => {
|
|||
pendingHistoryItems: [],
|
||||
thought: { subject: shortTitle },
|
||||
cancelOngoingRequest: vi.fn(),
|
||||
retryLastPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
// Act: Render the container
|
||||
|
|
@ -794,6 +799,7 @@ describe('AppContainer State Management', () => {
|
|||
pendingHistoryItems: [],
|
||||
thought: { subject: title },
|
||||
cancelOngoingRequest: vi.fn(),
|
||||
retryLastPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
// Act: Render the container
|
||||
|
|
@ -841,6 +847,7 @@ describe('AppContainer State Management', () => {
|
|||
pendingHistoryItems: [],
|
||||
thought: null,
|
||||
cancelOngoingRequest: vi.fn(),
|
||||
retryLastPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
// Act: Render the container
|
||||
|
|
@ -882,6 +889,7 @@ describe('AppContainer State Management', () => {
|
|||
pendingHistoryItems: [],
|
||||
thought: null,
|
||||
cancelOngoingRequest: vi.fn(),
|
||||
retryLastPrompt: vi.fn(),
|
||||
activePtyId: 'some-id',
|
||||
});
|
||||
|
||||
|
|
@ -1013,6 +1021,7 @@ describe('AppContainer State Management', () => {
|
|||
pendingHistoryItems: [],
|
||||
thought: null,
|
||||
cancelOngoingRequest: mockCancelOngoingRequest,
|
||||
retryLastPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
const mockHandleSlashCommand = vi.fn();
|
||||
|
|
|
|||
|
|
@ -629,6 +629,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
|||
pendingHistoryItems: pendingGeminiHistoryItems,
|
||||
thought,
|
||||
cancelOngoingRequest,
|
||||
retryLastPrompt,
|
||||
handleApprovalModeChange,
|
||||
activePtyId,
|
||||
loopDetectionConfirmationRequest,
|
||||
|
|
@ -1532,6 +1533,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
|||
onSuggestionsVisibilityChange: setHasSuggestionsVisible,
|
||||
refreshStatic,
|
||||
handleFinalSubmit,
|
||||
handleRetryLastPrompt: retryLastPrompt,
|
||||
handleClearScreen,
|
||||
// Welcome back dialog
|
||||
handleWelcomeBackSelection,
|
||||
|
|
@ -1575,6 +1577,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
|||
handleEscapePromptChange,
|
||||
refreshStatic,
|
||||
handleFinalSubmit,
|
||||
retryLastPrompt,
|
||||
handleClearScreen,
|
||||
handleWelcomeBackSelection,
|
||||
handleWelcomeBackClose,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ const createMockUIActions = (overrides: Partial<UIActions> = {}): UIActions => {
|
|||
// AuthDialog only uses handleAuthSelect
|
||||
const baseActions = {
|
||||
handleAuthSelect: vi.fn(),
|
||||
handleRetryLastPrompt: vi.fn(),
|
||||
} as Partial<UIActions>;
|
||||
|
||||
return {
|
||||
|
|
@ -169,9 +170,9 @@ describe('AuthDialog', () => {
|
|||
|
||||
const { lastFrame } = renderAuthDialog(settings);
|
||||
|
||||
// Since the auth dialog shows API-KEY option now,
|
||||
// Since the auth dialog shows API Key option now,
|
||||
// it won't show GEMINI_API_KEY messages
|
||||
expect(lastFrame()).toContain('API-KEY');
|
||||
expect(lastFrame()).toContain('API Key');
|
||||
});
|
||||
|
||||
it('should not show the GEMINI_API_KEY message if QWEN_DEFAULT_AUTH_TYPE is set to something else', () => {
|
||||
|
|
@ -257,9 +258,9 @@ describe('AuthDialog', () => {
|
|||
|
||||
const { lastFrame } = renderAuthDialog(settings);
|
||||
|
||||
// Since the auth dialog shows API-KEY option now,
|
||||
// Since the auth dialog shows API Key option now,
|
||||
// it won't show GEMINI_API_KEY messages
|
||||
expect(lastFrame()).toContain('API-KEY');
|
||||
expect(lastFrame()).toContain('API Key');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -305,7 +306,7 @@ describe('AuthDialog', () => {
|
|||
const { lastFrame } = renderAuthDialog(settings);
|
||||
|
||||
// QWEN_OAUTH is the first option, so it should be selected
|
||||
expect(lastFrame()).toContain('● 1. Qwen OAuth');
|
||||
expect(lastFrame()).toContain('Qwen OAuth');
|
||||
});
|
||||
|
||||
it('should fall back to default if QWEN_DEFAULT_AUTH_TYPE is not set', () => {
|
||||
|
|
@ -345,7 +346,7 @@ describe('AuthDialog', () => {
|
|||
const { lastFrame } = renderAuthDialog(settings);
|
||||
|
||||
// Default is Qwen OAuth (first option)
|
||||
expect(lastFrame()).toContain('● 1. Qwen OAuth');
|
||||
expect(lastFrame()).toContain('Qwen OAuth');
|
||||
});
|
||||
|
||||
it('should show an error and fall back to default if QWEN_DEFAULT_AUTH_TYPE is invalid', () => {
|
||||
|
|
@ -388,7 +389,7 @@ describe('AuthDialog', () => {
|
|||
|
||||
// Since the auth dialog doesn't show QWEN_DEFAULT_AUTH_TYPE errors anymore,
|
||||
// it will just show the default Qwen OAuth option
|
||||
expect(lastFrame()).toContain('● 1. Qwen OAuth');
|
||||
expect(lastFrame()).toContain('Qwen OAuth');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -11,16 +11,19 @@ import { Box, Text } from 'ink';
|
|||
import Link from 'ink-link';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { RadioButtonSelect } from '../components/shared/RadioButtonSelect.js';
|
||||
import { DescriptiveRadioButtonSelect } from '../components/shared/DescriptiveRadioButtonSelect.js';
|
||||
import { ApiKeyInput } from '../components/ApiKeyInput.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { useUIActions } from '../contexts/UIActionsContext.js';
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
import { CodingPlanRegion } from '../../constants/codingPlan.js';
|
||||
import {
|
||||
CodingPlanRegion,
|
||||
isCodingPlanConfig,
|
||||
} from '../../constants/codingPlan.js';
|
||||
|
||||
const MODEL_PROVIDERS_DOCUMENTATION_URL =
|
||||
'https://qwenlm.github.io/qwen-code-docs/en/users/configuration/settings/#modelproviders';
|
||||
'https://qwenlm.github.io/qwen-code-docs/en/users/configuration/model-providers/';
|
||||
|
||||
function parseDefaultAuthType(
|
||||
defaultAuthType: string | undefined,
|
||||
|
|
@ -34,11 +37,11 @@ function parseDefaultAuthType(
|
|||
return null;
|
||||
}
|
||||
|
||||
// Sub-mode types for API-KEY authentication
|
||||
type ApiKeySubMode = 'coding-plan' | 'coding-plan-intl' | 'custom';
|
||||
// Main menu option type
|
||||
type MainOption = typeof AuthType.QWEN_OAUTH | 'CODING_PLAN' | 'API_KEY';
|
||||
|
||||
// View level for navigation
|
||||
type ViewLevel = 'main' | 'api-key-sub' | 'api-key-input' | 'custom-info';
|
||||
type ViewLevel = 'main' | 'region-select' | 'api-key-input' | 'custom-info';
|
||||
|
||||
export function AuthDialog(): React.JSX.Element {
|
||||
const { pendingAuthType, authError } = useUIState();
|
||||
|
|
@ -50,58 +53,107 @@ export function AuthDialog(): React.JSX.Element {
|
|||
const config = useConfig();
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
||||
const [viewLevel, setViewLevel] = useState<ViewLevel>('main');
|
||||
const [apiKeySubModeIndex, setApiKeySubModeIndex] = useState<number>(0);
|
||||
const [regionIndex, setRegionIndex] = useState<number>(0);
|
||||
const [region, setRegion] = useState<CodingPlanRegion>(
|
||||
CodingPlanRegion.CHINA,
|
||||
);
|
||||
|
||||
// Main authentication entries
|
||||
// Main authentication entries (flat three-option layout)
|
||||
const mainItems = [
|
||||
{
|
||||
key: AuthType.QWEN_OAUTH,
|
||||
title: t('Qwen OAuth'),
|
||||
label: t('Qwen OAuth'),
|
||||
value: AuthType.QWEN_OAUTH,
|
||||
description: t(
|
||||
'Free \u00B7 Up to 1,000 requests/day \u00B7 Qwen latest models',
|
||||
),
|
||||
value: AuthType.QWEN_OAUTH as MainOption,
|
||||
},
|
||||
{
|
||||
key: 'API-KEY',
|
||||
label: t('API-KEY'),
|
||||
value: 'API-KEY' as const,
|
||||
key: 'CODING_PLAN',
|
||||
title: t('Alibaba Cloud Coding Plan'),
|
||||
label: t('Alibaba Cloud Coding Plan'),
|
||||
description: t(
|
||||
'Paid \u00B7 Up to 6,000 requests/5 hrs \u00B7 All Alibaba Cloud Coding Plan Models',
|
||||
),
|
||||
value: 'CODING_PLAN' as MainOption,
|
||||
},
|
||||
{
|
||||
key: 'API_KEY',
|
||||
title: t('API Key'),
|
||||
label: t('API Key'),
|
||||
description: t('Bring your own API key'),
|
||||
value: 'API_KEY' as MainOption,
|
||||
},
|
||||
];
|
||||
|
||||
// API-KEY sub-mode entries
|
||||
const apiKeySubItems = [
|
||||
// Region selection entries (shown after selecting Alibaba Cloud Coding Plan)
|
||||
const regionItems = [
|
||||
{
|
||||
key: 'coding-plan',
|
||||
label: t('Coding Plan (Bailian, China)'),
|
||||
value: 'coding-plan' as ApiKeySubMode,
|
||||
key: 'china',
|
||||
title: '阿里云百炼 (aliyun.com)',
|
||||
label: '阿里云百炼 (aliyun.com)',
|
||||
description: (
|
||||
<Link
|
||||
url="https://help.aliyun.com/zh/model-studio/coding-plan"
|
||||
fallback={false}
|
||||
>
|
||||
<Text color={theme.text.secondary}>
|
||||
https://help.aliyun.com/zh/model-studio/coding-plan
|
||||
</Text>
|
||||
</Link>
|
||||
),
|
||||
value: CodingPlanRegion.CHINA,
|
||||
},
|
||||
{
|
||||
key: 'coding-plan-intl',
|
||||
label: t('Coding Plan (Bailian, Global/Intl)'),
|
||||
value: 'coding-plan-intl' as ApiKeySubMode,
|
||||
},
|
||||
{
|
||||
key: 'custom',
|
||||
label: t('Custom'),
|
||||
value: 'custom' as ApiKeySubMode,
|
||||
key: 'global',
|
||||
title: 'Alibaba Cloud (alibabacloud.com)',
|
||||
label: 'Alibaba Cloud (alibabacloud.com)',
|
||||
description: (
|
||||
<Link
|
||||
url="https://www.alibabacloud.com/help/en/model-studio/coding-plan"
|
||||
fallback={false}
|
||||
>
|
||||
<Text color={theme.text.secondary}>
|
||||
https://www.alibabacloud.com/help/en/model-studio/coding-plan
|
||||
</Text>
|
||||
</Link>
|
||||
),
|
||||
value: CodingPlanRegion.GLOBAL,
|
||||
},
|
||||
];
|
||||
|
||||
// Map an AuthType to the corresponding main menu option.
|
||||
// QWEN_OAUTH maps directly; any other auth type maps to CODING_PLAN only
|
||||
// if the current config actually uses a Coding Plan baseUrl+envKey,
|
||||
// otherwise it maps to API_KEY.
|
||||
const contentGenConfig = config.getContentGeneratorConfig();
|
||||
const isCurrentlyCodingPlan =
|
||||
isCodingPlanConfig(
|
||||
contentGenConfig?.baseUrl,
|
||||
contentGenConfig?.apiKeyEnvKey,
|
||||
) !== false;
|
||||
|
||||
const authTypeToMainOption = (authType: AuthType): MainOption => {
|
||||
if (authType === AuthType.QWEN_OAUTH) return AuthType.QWEN_OAUTH;
|
||||
if (authType === AuthType.USE_OPENAI && isCurrentlyCodingPlan)
|
||||
return 'CODING_PLAN';
|
||||
return 'API_KEY';
|
||||
};
|
||||
|
||||
const initialAuthIndex = Math.max(
|
||||
0,
|
||||
mainItems.findIndex((item) => {
|
||||
// Priority 1: pendingAuthType
|
||||
if (pendingAuthType) {
|
||||
return item.value === pendingAuthType;
|
||||
return item.value === authTypeToMainOption(pendingAuthType);
|
||||
}
|
||||
|
||||
// Priority 2: config.getAuthType() - the source of truth
|
||||
const currentAuthType = config.getAuthType();
|
||||
if (currentAuthType) {
|
||||
return item.value === currentAuthType;
|
||||
return item.value === authTypeToMainOption(currentAuthType);
|
||||
}
|
||||
|
||||
// Priority 3: QWEN_DEFAULT_AUTH_TYPE env var
|
||||
|
|
@ -109,7 +161,7 @@ export function AuthDialog(): React.JSX.Element {
|
|||
process.env['QWEN_DEFAULT_AUTH_TYPE'],
|
||||
);
|
||||
if (defaultAuthType) {
|
||||
return item.value === defaultAuthType;
|
||||
return item.value === authTypeToMainOption(defaultAuthType);
|
||||
}
|
||||
|
||||
// Priority 4: default to QWEN_OAUTH
|
||||
|
|
@ -117,21 +169,19 @@ export function AuthDialog(): React.JSX.Element {
|
|||
}),
|
||||
);
|
||||
|
||||
const hasApiKey = Boolean(config.getContentGeneratorConfig()?.apiKey);
|
||||
const currentSelectedAuthType =
|
||||
selectedIndex !== null
|
||||
? mainItems[selectedIndex]?.value
|
||||
: mainItems[initialAuthIndex]?.value;
|
||||
|
||||
const handleMainSelect = async (
|
||||
value: (typeof mainItems)[number]['value'],
|
||||
) => {
|
||||
const handleMainSelect = async (value: MainOption) => {
|
||||
setErrorMessage(null);
|
||||
onAuthError(null);
|
||||
|
||||
if (value === 'API-KEY') {
|
||||
// Navigate to API-KEY sub-mode selection
|
||||
setViewLevel('api-key-sub');
|
||||
if (value === 'CODING_PLAN') {
|
||||
// Navigate to region selection
|
||||
setViewLevel('region-select');
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === 'API_KEY') {
|
||||
// Navigate directly to custom API key info
|
||||
setViewLevel('custom-info');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -139,19 +189,11 @@ export function AuthDialog(): React.JSX.Element {
|
|||
await onAuthSelect(value);
|
||||
};
|
||||
|
||||
const handleApiKeySubSelect = async (subMode: ApiKeySubMode) => {
|
||||
const handleRegionSelect = async (selectedRegion: CodingPlanRegion) => {
|
||||
setErrorMessage(null);
|
||||
onAuthError(null);
|
||||
|
||||
if (subMode === 'coding-plan') {
|
||||
setRegion(CodingPlanRegion.CHINA);
|
||||
setViewLevel('api-key-input');
|
||||
} else if (subMode === 'coding-plan-intl') {
|
||||
setRegion(CodingPlanRegion.GLOBAL);
|
||||
setViewLevel('api-key-input');
|
||||
} else {
|
||||
setViewLevel('custom-info');
|
||||
}
|
||||
setRegion(selectedRegion);
|
||||
setViewLevel('api-key-input');
|
||||
};
|
||||
|
||||
const handleApiKeyInputSubmit = async (apiKey: string) => {
|
||||
|
|
@ -170,12 +212,10 @@ export function AuthDialog(): React.JSX.Element {
|
|||
setErrorMessage(null);
|
||||
onAuthError(null);
|
||||
|
||||
if (viewLevel === 'api-key-sub') {
|
||||
if (viewLevel === 'region-select' || viewLevel === 'custom-info') {
|
||||
setViewLevel('main');
|
||||
// Reset selectedIndex to ensure UI syncs with initialAuthIndex
|
||||
setSelectedIndex(null);
|
||||
} else if (viewLevel === 'api-key-input' || viewLevel === 'custom-info') {
|
||||
setViewLevel('api-key-sub');
|
||||
} else if (viewLevel === 'api-key-input') {
|
||||
setViewLevel('region-select');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -183,7 +223,7 @@ export function AuthDialog(): React.JSX.Element {
|
|||
(key) => {
|
||||
if (key.name === 'escape') {
|
||||
// Handle Escape based on current view level
|
||||
if (viewLevel === 'api-key-sub') {
|
||||
if (viewLevel === 'region-select') {
|
||||
handleGoBack();
|
||||
return;
|
||||
}
|
||||
|
|
@ -215,62 +255,39 @@ export function AuthDialog(): React.JSX.Element {
|
|||
const renderMainView = () => (
|
||||
<>
|
||||
<Box marginTop={1}>
|
||||
<Text>{t('How would you like to authenticate for this project?')}</Text>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<RadioButtonSelect
|
||||
<DescriptiveRadioButtonSelect
|
||||
items={mainItems}
|
||||
initialIndex={initialAuthIndex}
|
||||
onSelect={handleMainSelect}
|
||||
onHighlight={(value) => {
|
||||
const index = mainItems.findIndex((item) => item.value === value);
|
||||
setSelectedIndex(index);
|
||||
}}
|
||||
itemGap={1}
|
||||
/>
|
||||
</Box>
|
||||
<Box marginTop={1} paddingLeft={2}>
|
||||
<Text color={theme.text.secondary}>
|
||||
{currentSelectedAuthType === AuthType.QWEN_OAUTH
|
||||
? t('Login with QwenChat account to use daily free quota.')
|
||||
: t('Use coding plan credentials or your own api-keys/providers.')}
|
||||
</Text>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
// Render API-KEY sub-mode selection
|
||||
const renderApiKeySubView = () => (
|
||||
// Render region selection for Alibaba Cloud Coding Plan
|
||||
const renderRegionSelectView = () => (
|
||||
<>
|
||||
<Box marginTop={1}>
|
||||
<Text>{t('Select API-KEY configuration mode:')}</Text>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<RadioButtonSelect
|
||||
items={apiKeySubItems}
|
||||
initialIndex={apiKeySubModeIndex}
|
||||
onSelect={handleApiKeySubSelect}
|
||||
onHighlight={(value) => {
|
||||
const index = apiKeySubItems.findIndex(
|
||||
(item) => item.value === value,
|
||||
);
|
||||
setApiKeySubModeIndex(index);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box marginTop={1} paddingLeft={2}>
|
||||
<Text color={theme.text.secondary}>
|
||||
{apiKeySubItems[apiKeySubModeIndex]?.value === 'custom'
|
||||
? t(
|
||||
'More instructions about configuring `modelProviders` manually.',
|
||||
)
|
||||
: t(
|
||||
"Paste your api key of Bailian Coding Plan and you're all set!",
|
||||
)}
|
||||
<Text color={theme.text.primary}>
|
||||
{t('Choose based on where your account is registered')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<DescriptiveRadioButtonSelect
|
||||
items={regionItems}
|
||||
initialIndex={regionIndex}
|
||||
onSelect={handleRegionSelect}
|
||||
onHighlight={(value) => {
|
||||
const index = regionItems.findIndex((item) => item.value === value);
|
||||
setRegionIndex(index);
|
||||
}}
|
||||
itemGap={1}
|
||||
/>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme?.text?.secondary}>
|
||||
{t('(Press Escape to go back)')}
|
||||
{t('Enter to select, ↑↓ to navigate, Esc to go back')}
|
||||
</Text>
|
||||
</Box>
|
||||
</>
|
||||
|
|
@ -291,68 +308,22 @@ export function AuthDialog(): React.JSX.Element {
|
|||
const renderCustomInfoView = () => (
|
||||
<>
|
||||
<Box marginTop={1}>
|
||||
<Text bold>{t('Custom API-KEY Configuration')}</Text>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text>
|
||||
{t('For advanced users who want to configure models manually.')}
|
||||
<Text color={theme.text.primary}>
|
||||
{t('You can configure your API key and models in settings.json')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text>{t('Please configure your models in settings.json:')}</Text>
|
||||
</Box>
|
||||
<Box marginTop={1} paddingLeft={2}>
|
||||
<Text color={theme.status.warning}>
|
||||
1. {t('Set API key via environment variable (e.g., OPENAI_API_KEY)')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginTop={0} paddingLeft={2}>
|
||||
<Text color={theme.status.warning}>
|
||||
2.{' '}
|
||||
{t(
|
||||
"Add model configuration to modelProviders['openai'] (or other auth types)",
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginTop={0} paddingLeft={2}>
|
||||
<Text color={theme.status.warning}>
|
||||
3.{' '}
|
||||
{t(
|
||||
'Each provider needs: id, envKey (required), plus optional baseUrl, generationConfig',
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginTop={0} paddingLeft={2}>
|
||||
<Text color={theme.status.warning}>
|
||||
4.{' '}
|
||||
{t(
|
||||
'Use /model command to select your preferred model from the configured list',
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme?.text?.secondary}>
|
||||
{t(
|
||||
'Supported auth types: openai, anthropic, gemini, vertex-ai, etc.',
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme?.text?.secondary} underline>
|
||||
{t('More instructions please check:')}
|
||||
</Text>
|
||||
<Text>{t('Refer to the documentation for setup instructions')}</Text>
|
||||
</Box>
|
||||
<Box marginTop={0}>
|
||||
<Link url={MODEL_PROVIDERS_DOCUMENTATION_URL} fallback={false}>
|
||||
<Text color={theme.status.success} underline>
|
||||
<Text color={theme.text.link}>
|
||||
{MODEL_PROVIDERS_DOCUMENTATION_URL}
|
||||
</Text>
|
||||
</Link>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme?.text?.secondary}>
|
||||
{t('(Press Escape to go back)')}
|
||||
</Text>
|
||||
<Text color={theme.text.secondary}>{t('Esc to go back')}</Text>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
|
@ -360,15 +331,15 @@ export function AuthDialog(): React.JSX.Element {
|
|||
const getViewTitle = () => {
|
||||
switch (viewLevel) {
|
||||
case 'main':
|
||||
return t('Get started');
|
||||
case 'api-key-sub':
|
||||
return t('API-KEY Configuration');
|
||||
return t('Select Authentication Method');
|
||||
case 'region-select':
|
||||
return t('Select Region for Coding Plan');
|
||||
case 'api-key-input':
|
||||
return t('Coding Plan Setup');
|
||||
return t('Enter Coding Plan API Key');
|
||||
case 'custom-info':
|
||||
return t('Custom Configuration');
|
||||
default:
|
||||
return t('Get started');
|
||||
return t('Select Authentication Method');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -383,7 +354,7 @@ export function AuthDialog(): React.JSX.Element {
|
|||
<Text bold>{getViewTitle()}</Text>
|
||||
|
||||
{viewLevel === 'main' && renderMainView()}
|
||||
{viewLevel === 'api-key-sub' && renderApiKeySubView()}
|
||||
{viewLevel === 'region-select' && renderRegionSelectView()}
|
||||
{viewLevel === 'api-key-input' && renderApiKeyInputView()}
|
||||
{viewLevel === 'custom-info' && renderCustomInfoView()}
|
||||
|
||||
|
|
@ -395,31 +366,28 @@ export function AuthDialog(): React.JSX.Element {
|
|||
|
||||
{viewLevel === 'main' && (
|
||||
<>
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.text.accent}>
|
||||
{t('(Use Enter to Set Auth)')}
|
||||
{/* <Box marginTop={1}>
|
||||
<Text color={theme.text.secondary}>
|
||||
{t('Enter to select, \u2191\u2193 to navigate, Esc to close')}
|
||||
</Text>
|
||||
</Box> */}
|
||||
<Box marginY={1}>
|
||||
<Text color={theme.border.default}>{'\u2500'.repeat(80)}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>
|
||||
{t('Terms of Services and Privacy Notice')}:
|
||||
</Text>
|
||||
</Box>
|
||||
{hasApiKey && currentSelectedAuthType === AuthType.QWEN_OAUTH && (
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme?.text?.secondary}>
|
||||
{t(
|
||||
'Note: Your existing API key in settings.json will not be cleared when using Qwen OAuth. You can switch back to OpenAI authentication later if needed.',
|
||||
)}
|
||||
<Box>
|
||||
<Link
|
||||
url="https://qwenlm.github.io/qwen-code-docs/en/users/support/tos-privacy/"
|
||||
fallback={false}
|
||||
>
|
||||
<Text color={theme.text.secondary} underline>
|
||||
https://qwenlm.github.io/qwen-code-docs/en/users/support/tos-privacy/
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box marginTop={1}>
|
||||
<Text>
|
||||
{t('Terms of Services and Privacy Notice for Qwen Code')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.text.link}>
|
||||
{
|
||||
'https://qwenlm.github.io/qwen-code-docs/en/users/support/tos-privacy/'
|
||||
}
|
||||
</Text>
|
||||
</Link>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -300,7 +300,7 @@ export const useAuthCommand = (
|
|||
setAuthError(null);
|
||||
|
||||
// Get configuration based on region
|
||||
const { template, version, regionName } = getCodingPlanConfig(region);
|
||||
const { template, version } = getCodingPlanConfig(region);
|
||||
|
||||
// Get persist scope
|
||||
const persistScope = getPersistScopeForModelSelection(settings);
|
||||
|
|
@ -390,7 +390,7 @@ export const useAuthCommand = (
|
|||
type: MessageType.INFO,
|
||||
text: t(
|
||||
'Authenticated successfully with {{region}}. API key and model configs saved to settings.json (backed up).',
|
||||
{ region: regionName },
|
||||
{ region: t('Alibaba Cloud Coding Plan') },
|
||||
),
|
||||
},
|
||||
Date.now(),
|
||||
|
|
|
|||
|
|
@ -49,6 +49,18 @@ export function ApiKeyInput({
|
|||
setError(t('API key cannot be empty.'));
|
||||
return;
|
||||
}
|
||||
// Only validate sk-sp- prefix for China region (aliyun.com)
|
||||
if (
|
||||
region === CodingPlanRegion.CHINA &&
|
||||
!trimmedKey.startsWith('sk-sp-')
|
||||
) {
|
||||
setError(
|
||||
t(
|
||||
'Invalid API key. Coding Plan API keys start with "sk-sp-". Please check.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
onSubmit(trimmedKey);
|
||||
}
|
||||
},
|
||||
|
|
@ -57,9 +69,6 @@ export function ApiKeyInput({
|
|||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box marginBottom={1}>
|
||||
<Text>{t('Please enter your API key:')}</Text>
|
||||
</Box>
|
||||
<TextInput value={apiKey} onChange={setApiKey} placeholder="sk-sp-..." />
|
||||
{error && (
|
||||
<Box marginTop={1}>
|
||||
|
|
@ -67,18 +76,18 @@ export function ApiKeyInput({
|
|||
</Box>
|
||||
)}
|
||||
<Box marginTop={1}>
|
||||
<Text>{t('You can get your exclusive Coding Plan API-KEY here:')}</Text>
|
||||
<Text>{t('You can get your Coding Plan API key here')}</Text>
|
||||
</Box>
|
||||
<Box marginTop={0}>
|
||||
<Link url={apiKeyUrl} fallback={false}>
|
||||
<Text color={theme.status.success} underline>
|
||||
<Text color={theme.text.link} underline>
|
||||
{apiKeyUrl}
|
||||
</Text>
|
||||
</Link>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.text.secondary}>
|
||||
{t('(Press Enter to submit, Escape to cancel)')}
|
||||
{t('Enter to submit, Esc to go back')}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -5,16 +5,43 @@
|
|||
*/
|
||||
|
||||
import { Box } from 'ink';
|
||||
import { Header } from './Header.js';
|
||||
import { AuthType } from '@qwen-code/qwen-code-core';
|
||||
import { Header, AuthDisplayType } from './Header.js';
|
||||
import { Tips } from './Tips.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { isCodingPlanConfig } from '../../constants/codingPlan.js';
|
||||
|
||||
interface AppHeaderProps {
|
||||
version: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the auth display type based on auth type and configuration.
|
||||
*/
|
||||
function getAuthDisplayType(
|
||||
authType?: AuthType,
|
||||
baseUrl?: string,
|
||||
apiKeyEnvKey?: string,
|
||||
): AuthDisplayType {
|
||||
if (!authType) {
|
||||
return AuthDisplayType.UNKNOWN;
|
||||
}
|
||||
|
||||
// Check if it's a Coding Plan config
|
||||
if (isCodingPlanConfig(baseUrl, apiKeyEnvKey)) {
|
||||
return AuthDisplayType.CODING_PLAN;
|
||||
}
|
||||
|
||||
switch (authType) {
|
||||
case AuthType.QWEN_OAUTH:
|
||||
return AuthDisplayType.QWEN_OAUTH;
|
||||
default:
|
||||
return AuthDisplayType.API_KEY;
|
||||
}
|
||||
}
|
||||
|
||||
export const AppHeader = ({ version }: AppHeaderProps) => {
|
||||
const settings = useSettings();
|
||||
const config = useConfig();
|
||||
|
|
@ -27,12 +54,18 @@ export const AppHeader = ({ version }: AppHeaderProps) => {
|
|||
const showBanner = !config.getScreenReader();
|
||||
const showTips = !(settings.merged.ui?.hideTips || config.getScreenReader());
|
||||
|
||||
const authDisplayType = getAuthDisplayType(
|
||||
authType,
|
||||
contentGeneratorConfig?.baseUrl,
|
||||
contentGeneratorConfig?.apiKeyEnvKey,
|
||||
);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{showBanner && (
|
||||
<Header
|
||||
version={version}
|
||||
authType={authType}
|
||||
authDisplayType={authDisplayType}
|
||||
model={model}
|
||||
workingDirectory={targetDir}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ const createMockUIState = (overrides: Partial<UIState> = {}): UIState =>
|
|||
const createMockUIActions = (): UIActions =>
|
||||
({
|
||||
handleFinalSubmit: vi.fn(),
|
||||
handleRetryLastPrompt: vi.fn(),
|
||||
handleClearScreen: vi.fn(),
|
||||
setShellModeActive: vi.fn(),
|
||||
onEscapePromptChange: vi.fn(),
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@
|
|||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { AuthType } from '@qwen-code/qwen-code-core';
|
||||
import { Header } from './Header.js';
|
||||
import { Header, AuthDisplayType } from './Header.js';
|
||||
import * as useTerminalSize from '../hooks/useTerminalSize.js';
|
||||
|
||||
vi.mock('../hooks/useTerminalSize.js');
|
||||
|
|
@ -15,86 +14,70 @@ const useTerminalSizeMock = vi.mocked(useTerminalSize.useTerminalSize);
|
|||
|
||||
const defaultProps = {
|
||||
version: '1.0.0',
|
||||
authType: AuthType.QWEN_OAUTH,
|
||||
authDisplayType: AuthDisplayType.QWEN_OAUTH,
|
||||
model: 'qwen-coder-plus',
|
||||
workingDirectory: '/home/user/projects/test',
|
||||
};
|
||||
|
||||
describe('<Header />', () => {
|
||||
beforeEach(() => {
|
||||
// Default to wide terminal (shows both logo and info panel)
|
||||
useTerminalSizeMock.mockReturnValue({ columns: 120, rows: 24 });
|
||||
});
|
||||
|
||||
it('renders the ASCII logo on wide terminal', () => {
|
||||
const { lastFrame } = render(<Header {...defaultProps} />);
|
||||
// Check that parts of the shortAsciiLogo are rendered
|
||||
expect(lastFrame()).toContain('██╔═══██╗');
|
||||
});
|
||||
|
||||
it('hides the ASCII logo on narrow terminal', () => {
|
||||
useTerminalSizeMock.mockReturnValue({ columns: 60, rows: 24 });
|
||||
const { lastFrame } = render(<Header {...defaultProps} />);
|
||||
// Should not contain the logo but still show the info panel
|
||||
expect(lastFrame()).not.toContain('██╔═══██╗');
|
||||
expect(lastFrame()).toContain('>_ Qwen Code');
|
||||
});
|
||||
|
||||
it('renders custom ASCII art when provided on wide terminal', () => {
|
||||
const customArt = 'CUSTOM ART';
|
||||
const { lastFrame } = render(
|
||||
<Header {...defaultProps} customAsciiArt={customArt} />,
|
||||
);
|
||||
expect(lastFrame()).toContain(customArt);
|
||||
});
|
||||
|
||||
it('displays the version number', () => {
|
||||
const { lastFrame } = render(<Header {...defaultProps} />);
|
||||
expect(lastFrame()).toContain('v1.0.0');
|
||||
});
|
||||
|
||||
it('displays Qwen Code title with >_ prefix', () => {
|
||||
const { lastFrame } = render(<Header {...defaultProps} />);
|
||||
expect(lastFrame()).toContain('>_ Qwen Code');
|
||||
});
|
||||
|
||||
it('displays auth type and model', () => {
|
||||
const { lastFrame } = render(<Header {...defaultProps} />);
|
||||
expect(lastFrame()).toContain('Qwen OAuth');
|
||||
expect(lastFrame()).toContain('qwen-coder-plus');
|
||||
});
|
||||
|
||||
it('displays Coding Plan auth type', () => {
|
||||
const { lastFrame } = render(
|
||||
<Header
|
||||
{...defaultProps}
|
||||
authDisplayType={AuthDisplayType.CODING_PLAN}
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toContain('Coding Plan');
|
||||
});
|
||||
|
||||
it('displays API Key auth type', () => {
|
||||
const { lastFrame } = render(
|
||||
<Header {...defaultProps} authDisplayType={AuthDisplayType.API_KEY} />,
|
||||
);
|
||||
expect(lastFrame()).toContain('API Key');
|
||||
});
|
||||
|
||||
it('displays Unknown when auth type is not set', () => {
|
||||
const { lastFrame } = render(
|
||||
<Header {...defaultProps} authDisplayType={undefined} />,
|
||||
);
|
||||
expect(lastFrame()).toContain('Unknown');
|
||||
});
|
||||
|
||||
it('displays working directory', () => {
|
||||
const { lastFrame } = render(<Header {...defaultProps} />);
|
||||
expect(lastFrame()).toContain('/home/user/projects/test');
|
||||
});
|
||||
|
||||
it('renders a custom working directory display', () => {
|
||||
const { lastFrame } = render(
|
||||
<Header {...defaultProps} workingDirectory="custom display" />,
|
||||
);
|
||||
expect(lastFrame()).toContain('custom display');
|
||||
});
|
||||
|
||||
it('displays working directory without branch name', () => {
|
||||
const { lastFrame } = render(<Header {...defaultProps} />);
|
||||
// Branch name is no longer shown in header
|
||||
expect(lastFrame()).toContain('/home/user/projects/test');
|
||||
expect(lastFrame()).not.toContain('(main*)');
|
||||
});
|
||||
|
||||
it('formats home directory with tilde', () => {
|
||||
const { lastFrame } = render(
|
||||
<Header {...defaultProps} workingDirectory="/Users/testuser/projects" />,
|
||||
);
|
||||
// The actual home dir replacement depends on os.homedir()
|
||||
// Just verify the path is shown
|
||||
expect(lastFrame()).toContain('projects');
|
||||
});
|
||||
|
||||
it('renders with border around info panel', () => {
|
||||
const { lastFrame } = render(<Header {...defaultProps} />);
|
||||
// Check for border characters (round border style uses these)
|
||||
expect(lastFrame()).toContain('╭');
|
||||
expect(lastFrame()).toContain('╯');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,59 +7,35 @@
|
|||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import Gradient from 'ink-gradient';
|
||||
import { AuthType, shortenPath, tildeifyPath } from '@qwen-code/qwen-code-core';
|
||||
import { shortenPath, tildeifyPath } from '@qwen-code/qwen-code-core';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { shortAsciiLogo } from './AsciiArt.js';
|
||||
import { getAsciiArtWidth, getCachedStringWidth } from '../utils/textUtils.js';
|
||||
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
||||
|
||||
/**
|
||||
* Auth display type for the Header component.
|
||||
* Simplified representation of authentication method shown to users.
|
||||
*/
|
||||
export enum AuthDisplayType {
|
||||
QWEN_OAUTH = 'Qwen OAuth',
|
||||
CODING_PLAN = 'Coding Plan',
|
||||
API_KEY = 'API Key',
|
||||
UNKNOWN = 'Unknown',
|
||||
}
|
||||
|
||||
interface HeaderProps {
|
||||
customAsciiArt?: string; // For user-defined ASCII art
|
||||
version: string;
|
||||
authType?: AuthType;
|
||||
authDisplayType?: AuthDisplayType;
|
||||
model: string;
|
||||
workingDirectory: string;
|
||||
}
|
||||
|
||||
function titleizeAuthType(value: string): string {
|
||||
return value
|
||||
.split(/[-_]/g)
|
||||
.filter(Boolean)
|
||||
.map((part) => {
|
||||
if (part.toLowerCase() === 'ai') {
|
||||
return 'AI';
|
||||
}
|
||||
return part.charAt(0).toUpperCase() + part.slice(1);
|
||||
})
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
// Format auth type for display
|
||||
function formatAuthType(authType?: AuthType): string {
|
||||
if (!authType) {
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
switch (authType) {
|
||||
case AuthType.QWEN_OAUTH:
|
||||
return 'Qwen OAuth';
|
||||
case AuthType.USE_OPENAI:
|
||||
return 'OpenAI';
|
||||
case AuthType.USE_GEMINI:
|
||||
return 'Gemini';
|
||||
case AuthType.USE_VERTEX_AI:
|
||||
return 'Vertex AI';
|
||||
case AuthType.USE_ANTHROPIC:
|
||||
return 'Anthropic';
|
||||
default:
|
||||
return titleizeAuthType(String(authType));
|
||||
}
|
||||
}
|
||||
|
||||
export const Header: React.FC<HeaderProps> = ({
|
||||
customAsciiArt,
|
||||
version,
|
||||
authType,
|
||||
authDisplayType,
|
||||
model,
|
||||
workingDirectory,
|
||||
}) => {
|
||||
|
|
@ -67,7 +43,7 @@ export const Header: React.FC<HeaderProps> = ({
|
|||
|
||||
const displayLogo = customAsciiArt ?? shortAsciiLogo;
|
||||
const logoWidth = getAsciiArtWidth(displayLogo);
|
||||
const formattedAuthType = formatAuthType(authType);
|
||||
const formattedAuthType = authDisplayType ?? AuthDisplayType.UNKNOWN;
|
||||
|
||||
// Calculate available space properly:
|
||||
// First determine if logo can be shown, then use remaining space for path
|
||||
|
|
@ -95,7 +71,7 @@ export const Header: React.FC<HeaderProps> = ({
|
|||
? Math.min(availableTerminalWidth - logoWidth - logoGap, maxInfoPanelWidth)
|
||||
: availableTerminalWidth;
|
||||
|
||||
// Calculate max path length (subtract padding/borders from available space)
|
||||
// Calculate max path lengths (subtract padding/borders from available space)
|
||||
const maxPathLength = Math.max(
|
||||
0,
|
||||
availableInfoPanelWidth - infoPanelChromeWidth,
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
|
|||
<WarningMessage text={itemForDisplay.text} />
|
||||
)}
|
||||
{itemForDisplay.type === 'error' && (
|
||||
<ErrorMessage text={itemForDisplay.text} />
|
||||
<ErrorMessage text={itemForDisplay.text} hint={itemForDisplay.hint} />
|
||||
)}
|
||||
{itemForDisplay.type === 'retry_countdown' && (
|
||||
<RetryCountdownMessage text={itemForDisplay.text} />
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ vi.mock('../contexts/UIStateContext.js', () => ({
|
|||
}));
|
||||
vi.mock('../contexts/UIActionsContext.js', () => ({
|
||||
useUIActions: vi.fn(() => ({
|
||||
handleRetryLastPrompt: vi.fn(),
|
||||
temporaryCloseFeedbackDialog: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
|
@ -2436,6 +2437,140 @@ describe('InputPrompt', () => {
|
|||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Ctrl+Y (RETRY_LAST) shortcut tests
|
||||
*
|
||||
* The Ctrl+Y shortcut should trigger handleRetryLastPrompt when:
|
||||
* 1. The user presses Ctrl+Y
|
||||
* 2. The InputPrompt is focused
|
||||
* 3. No other modal/dialog is open that would consume the key
|
||||
*
|
||||
* This shortcut is handled in InputPrompt.tsx at line 585-588:
|
||||
* if (keyMatchers[Command.RETRY_LAST](key)) {
|
||||
* uiActions.handleRetryLastPrompt();
|
||||
* return;
|
||||
* }
|
||||
*/
|
||||
describe('Ctrl+Y retry shortcut', () => {
|
||||
let mockUIActions: {
|
||||
handleRetryLastPrompt: ReturnType<typeof vi.fn>;
|
||||
temporaryCloseFeedbackDialog: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockUIActions = {
|
||||
handleRetryLastPrompt: vi.fn(),
|
||||
temporaryCloseFeedbackDialog: vi.fn(),
|
||||
};
|
||||
|
||||
// Override the mock for useUIActions
|
||||
vi.doMock('../contexts/UIActionsContext.js', () => ({
|
||||
useUIActions: vi.fn(() => mockUIActions),
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.doUnmock('../contexts/UIActionsContext.js');
|
||||
});
|
||||
|
||||
/**
|
||||
* Ctrl+Y should trigger handleRetryLastPrompt to retry the last failed request.
|
||||
* This is the primary activation path for the retry feature.
|
||||
*/
|
||||
it('should trigger handleRetryLastPrompt on Ctrl+Y', async () => {
|
||||
const { stdin, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await wait();
|
||||
|
||||
// Send Ctrl+Y (ASCII 25)
|
||||
stdin.write('\x19');
|
||||
await wait();
|
||||
|
||||
// The key matcher should have been triggered
|
||||
// Note: In the actual implementation, this would call uiActions.handleRetryLastPrompt()
|
||||
unmount();
|
||||
});
|
||||
|
||||
/**
|
||||
* The 'y' key alone (without Ctrl) should NOT trigger retry.
|
||||
* This ensures the shortcut doesn't interfere with normal typing.
|
||||
*/
|
||||
it('should NOT trigger retry on plain y key', async () => {
|
||||
const { stdin, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await wait();
|
||||
|
||||
// Send plain 'y'
|
||||
stdin.write('y');
|
||||
await wait();
|
||||
|
||||
// Should insert 'y' into buffer, not trigger retry
|
||||
expect(mockBuffer.handleInput).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'y',
|
||||
sequence: 'y',
|
||||
}),
|
||||
);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
/**
|
||||
* Ctrl+R should NOT trigger retry - it should trigger reverse search instead.
|
||||
* This ensures the retry shortcut doesn't conflict with existing shortcuts.
|
||||
*/
|
||||
it('should NOT trigger retry on Ctrl+R (reverse search)', async () => {
|
||||
const { stdin, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await wait();
|
||||
|
||||
// Send Ctrl+R (ASCII 18)
|
||||
stdin.write('\x12');
|
||||
await wait();
|
||||
|
||||
// Should activate reverse search, not retry
|
||||
// Verify the input was handled (not ignored)
|
||||
expect(mockBuffer.handleInput).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
ctrl: true,
|
||||
name: 'y',
|
||||
}),
|
||||
);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
/**
|
||||
* When feedback dialog is open, Ctrl+Y should be passed through after
|
||||
* temporarily closing the dialog.
|
||||
*/
|
||||
it('should handle Ctrl+Y when feedback dialog is open', async () => {
|
||||
// Mock feedback dialog as open
|
||||
const mockUIState = { isFeedbackDialogOpen: true };
|
||||
vi.doMock('../contexts/UIStateContext.js', () => ({
|
||||
useUIState: vi.fn(() => mockUIState),
|
||||
}));
|
||||
|
||||
const { stdin, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await wait();
|
||||
|
||||
// Send Ctrl+Y
|
||||
stdin.write('\x19');
|
||||
await wait();
|
||||
|
||||
// Dialog should be temporarily closed
|
||||
// Note: In actual implementation, temporaryCloseFeedbackDialog would be called
|
||||
|
||||
vi.doUnmock('../contexts/UIStateContext.js');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
function clean(str: string | undefined): string {
|
||||
if (!str) return '';
|
||||
|
|
|
|||
|
|
@ -582,6 +582,16 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
|||
return;
|
||||
}
|
||||
|
||||
// Ctrl+Y: Retry the last failed request.
|
||||
// This shortcut is available when:
|
||||
// - There is a failed request in the current session
|
||||
// - The stream is not currently responding or waiting for confirmation
|
||||
// If no failed request exists, a message will be shown to the user.
|
||||
if (keyMatchers[Command.RETRY_LAST](key)) {
|
||||
uiActions.handleRetryLastPrompt();
|
||||
return;
|
||||
}
|
||||
|
||||
if (shellModeActive && keyMatchers[Command.REVERSE_SEARCH](key)) {
|
||||
setReverseSearchActive(true);
|
||||
setTextBeforeReverseSearch(buffer.text);
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ const getShortcuts = (): Shortcut[] => [
|
|||
{ key: getNewlineKey(), description: t('for newline') + ' ⏎' },
|
||||
{ key: 'ctrl+l', description: t('to clear screen') },
|
||||
{ key: 'ctrl+r', description: t('to search history') },
|
||||
{ key: 'ctrl+y', description: t('to retry last request') },
|
||||
{ key: getPasteKey(), description: t('to paste images') },
|
||||
{ key: getExternalEditorKey(), description: t('for external editor') },
|
||||
];
|
||||
|
|
@ -54,11 +55,11 @@ const COLUMN_GAP = 4;
|
|||
const MARGIN_LEFT = 2;
|
||||
const MARGIN_RIGHT = 2;
|
||||
|
||||
// Column distribution for different layouts (3+4+4 for 3 cols, 6+5 for 2 cols)
|
||||
// Column distribution for different layouts (4+4+4 for 3 cols, 6+6 for 2 cols)
|
||||
const COLUMN_SPLITS: Record<number, number[]> = {
|
||||
3: [3, 4, 4],
|
||||
2: [6, 5],
|
||||
1: [11],
|
||||
3: [4, 4, 4],
|
||||
2: [6, 6],
|
||||
1: [12],
|
||||
};
|
||||
|
||||
export const KeyboardShortcuts: React.FC = () => {
|
||||
|
|
|
|||
|
|
@ -114,10 +114,9 @@ describe('<ModelDialog />', () => {
|
|||
cleanup();
|
||||
});
|
||||
|
||||
it('renders the title and help text', () => {
|
||||
it('renders the title', () => {
|
||||
const { getByText } = renderComponent();
|
||||
expect(getByText('Select Model')).toBeDefined();
|
||||
expect(getByText('(Press Esc to close)')).toBeDefined();
|
||||
});
|
||||
|
||||
it('passes all model options to DescriptiveRadioButtonSelect', () => {
|
||||
|
|
@ -289,11 +288,12 @@ describe('<ModelDialog />', () => {
|
|||
expect(props.onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not pass onHighlight to DescriptiveRadioButtonSelect', () => {
|
||||
it('passes onHighlight to DescriptiveRadioButtonSelect', () => {
|
||||
renderComponent();
|
||||
|
||||
const childOnHighlight = mockedSelect.mock.calls[0][0].onHighlight;
|
||||
expect(childOnHighlight).toBeUndefined();
|
||||
expect(childOnHighlight).toBeDefined();
|
||||
expect(typeof childOnHighlight).toBe('function');
|
||||
});
|
||||
|
||||
it('calls onClose prop when "escape" key is pressed', () => {
|
||||
|
|
|
|||
|
|
@ -14,8 +14,7 @@ import {
|
|||
MAINLINE_CODER_MODEL,
|
||||
type AvailableModel as CoreAvailableModel,
|
||||
type ContentGeneratorConfig,
|
||||
type ContentGeneratorConfigSource,
|
||||
type ContentGeneratorConfigSources,
|
||||
type InputModalities,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
|
|
@ -26,61 +25,25 @@ import { useSettings } from '../contexts/SettingsContext.js';
|
|||
import { getPersistScopeForModelSelection } from '../../config/modelProvidersScope.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
function formatModalities(modalities?: InputModalities): string {
|
||||
if (!modalities) return t('text-only');
|
||||
const parts: string[] = [];
|
||||
if (modalities.image) parts.push(t('image'));
|
||||
if (modalities.pdf) parts.push(t('pdf'));
|
||||
if (modalities.audio) parts.push(t('audio'));
|
||||
if (modalities.video) parts.push(t('video'));
|
||||
if (parts.length === 0) return t('text-only');
|
||||
return `${t('text')} · ${parts.join(' · ')}`;
|
||||
}
|
||||
|
||||
interface ModelDialogProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function formatSourceBadge(
|
||||
source: ContentGeneratorConfigSource | undefined,
|
||||
): string | undefined {
|
||||
if (!source) return undefined;
|
||||
|
||||
switch (source.kind) {
|
||||
case 'cli':
|
||||
return source.detail ? `CLI ${source.detail}` : 'CLI';
|
||||
case 'env':
|
||||
return source.envKey ? `ENV ${source.envKey}` : 'ENV';
|
||||
case 'settings':
|
||||
return source.settingsPath
|
||||
? `Settings ${source.settingsPath}`
|
||||
: 'Settings';
|
||||
case 'modelProviders': {
|
||||
const suffix =
|
||||
source.authType && source.modelId
|
||||
? `${source.authType}:${source.modelId}`
|
||||
: source.authType
|
||||
? `${source.authType}`
|
||||
: source.modelId
|
||||
? `${source.modelId}`
|
||||
: '';
|
||||
return suffix ? `ModelProviders ${suffix}` : 'ModelProviders';
|
||||
}
|
||||
case 'default':
|
||||
return source.detail ? `Default ${source.detail}` : 'Default';
|
||||
case 'computed':
|
||||
return source.detail ? `Computed ${source.detail}` : 'Computed';
|
||||
case 'programmatic':
|
||||
return source.detail ? `Programmatic ${source.detail}` : 'Programmatic';
|
||||
case 'unknown':
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function readSourcesFromConfig(config: unknown): ContentGeneratorConfigSources {
|
||||
if (!config) {
|
||||
return {};
|
||||
}
|
||||
const maybe = config as {
|
||||
getContentGeneratorConfigSources?: () => ContentGeneratorConfigSources;
|
||||
};
|
||||
return maybe.getContentGeneratorConfigSources?.() ?? {};
|
||||
}
|
||||
|
||||
function maskApiKey(apiKey: string | undefined): string {
|
||||
if (!apiKey) return '(not set)';
|
||||
if (!apiKey) return `(${t('not set')})`;
|
||||
const trimmed = apiKey.trim();
|
||||
if (trimmed.length === 0) return '(not set)';
|
||||
if (trimmed.length === 0) return `(${t('not set')})`;
|
||||
if (trimmed.length <= 6) return '***';
|
||||
const head = trimmed.slice(0, 3);
|
||||
const tail = trimmed.slice(-4);
|
||||
|
|
@ -131,7 +94,7 @@ function handleModelSwitchSuccess({
|
|||
{
|
||||
type: 'info',
|
||||
text:
|
||||
`authType: ${effectiveAuthType ?? '(none)'}` +
|
||||
`authType: ${effectiveAuthType ?? `(${t('none')})`}` +
|
||||
`\n` +
|
||||
`Using ${isRuntime ? 'runtime ' : ''}model: ${effectiveModelId}` +
|
||||
`\n` +
|
||||
|
|
@ -143,35 +106,26 @@ function handleModelSwitchSuccess({
|
|||
);
|
||||
}
|
||||
|
||||
function ConfigRow({
|
||||
function formatContextWindow(size?: number): string {
|
||||
if (!size) return `(${t('unknown')})`;
|
||||
return `${size.toLocaleString('en-US')} tokens`;
|
||||
}
|
||||
|
||||
function DetailRow({
|
||||
label,
|
||||
value,
|
||||
badge,
|
||||
}: {
|
||||
label: string;
|
||||
value: React.ReactNode;
|
||||
badge?: string;
|
||||
}): React.JSX.Element {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box>
|
||||
<Box minWidth={12} flexShrink={0}>
|
||||
<Text color={theme.text.secondary}>{label}:</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} flexDirection="row" flexWrap="wrap">
|
||||
<Text>{value}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Box minWidth={16} flexShrink={0}>
|
||||
<Text color={theme.text.secondary}>{label}:</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} flexDirection="row" flexWrap="wrap">
|
||||
<Text>{value}</Text>
|
||||
</Box>
|
||||
{badge ? (
|
||||
<Box>
|
||||
<Box minWidth={12} flexShrink={0}>
|
||||
<Text> </Text>
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
<Text color={theme.text.secondary}>{badge}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -183,13 +137,9 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
|||
|
||||
// Local error state for displaying errors within the dialog
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [highlightedValue, setHighlightedValue] = useState<string | null>(null);
|
||||
|
||||
const authType = config?.getAuthType();
|
||||
const effectiveConfig =
|
||||
(config?.getContentGeneratorConfig?.() as
|
||||
| ContentGeneratorConfig
|
||||
| undefined) ?? undefined;
|
||||
const sources = readSourcesFromConfig(config);
|
||||
|
||||
const availableModelEntries = useMemo(() => {
|
||||
const allModels = config ? config.getAllConfiguredModels() : [];
|
||||
|
|
@ -319,6 +269,20 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
|||
return index === -1 ? 0 : index;
|
||||
}, [MODEL_OPTIONS, preferredKey]);
|
||||
|
||||
const handleHighlight = useCallback((value: string) => {
|
||||
setHighlightedValue(value);
|
||||
}, []);
|
||||
|
||||
const highlightedEntry = useMemo(() => {
|
||||
const key = highlightedValue ?? preferredKey;
|
||||
return availableModelEntries.find(
|
||||
({ authType: t2, model, isRuntime, snapshotId }) => {
|
||||
const v = isRuntime && snapshotId ? snapshotId : `${t2}::${model.id}`;
|
||||
return v === key;
|
||||
},
|
||||
);
|
||||
}, [highlightedValue, preferredKey, availableModelEntries]);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
async (selected: string) => {
|
||||
setErrorMessage(null);
|
||||
|
|
@ -413,35 +377,6 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
|||
>
|
||||
<Text bold>{t('Select Model')}</Text>
|
||||
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text color={theme.text.secondary}>
|
||||
{t('Current (effective) configuration')}
|
||||
</Text>
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<ConfigRow label="AuthType" value={authType} />
|
||||
<ConfigRow
|
||||
label="Model"
|
||||
value={effectiveConfig?.model ?? config?.getModel?.() ?? ''}
|
||||
badge={formatSourceBadge(sources['model'])}
|
||||
/>
|
||||
|
||||
{authType !== AuthType.QWEN_OAUTH && (
|
||||
<>
|
||||
<ConfigRow
|
||||
label="Base URL"
|
||||
value={effectiveConfig?.baseUrl ?? t('(default)')}
|
||||
badge={formatSourceBadge(sources['baseUrl'])}
|
||||
/>
|
||||
<ConfigRow
|
||||
label="API Key"
|
||||
value={effectiveConfig?.apiKey ? t('(set)') : t('(not set)')}
|
||||
badge={formatSourceBadge(sources['apiKey'])}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{!hasModels ? (
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text color={theme.status.warning}>
|
||||
|
|
@ -465,12 +400,48 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
|||
<DescriptiveRadioButtonSelect
|
||||
items={MODEL_OPTIONS}
|
||||
onSelect={handleSelect}
|
||||
onHighlight={handleHighlight}
|
||||
initialIndex={initialIndex}
|
||||
showNumbers={true}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{highlightedEntry && (
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderTop
|
||||
borderBottom={false}
|
||||
borderLeft={false}
|
||||
borderRight={false}
|
||||
borderColor={theme.border.default}
|
||||
/>
|
||||
<DetailRow
|
||||
label={t('Modality')}
|
||||
value={formatModalities(highlightedEntry.model.modalities)}
|
||||
/>
|
||||
<DetailRow
|
||||
label={t('Context Window')}
|
||||
value={formatContextWindow(
|
||||
highlightedEntry.model.contextWindowSize,
|
||||
)}
|
||||
/>
|
||||
{highlightedEntry.authType !== AuthType.QWEN_OAUTH && (
|
||||
<>
|
||||
<DetailRow
|
||||
label="Base URL"
|
||||
value={highlightedEntry.model.baseUrl ?? t('(default)')}
|
||||
/>
|
||||
<DetailRow
|
||||
label="API Key"
|
||||
value={highlightedEntry.model.envKey ?? t('(not set)')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{errorMessage && (
|
||||
<Box marginTop={1} flexDirection="column" paddingX={1}>
|
||||
<Text color={theme.status.error} wrap="wrap">
|
||||
|
|
@ -480,7 +451,9 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
|||
)}
|
||||
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text color={theme.text.secondary}>{t('(Press Esc to close)')}</Text>
|
||||
<Text color={theme.text.secondary}>
|
||||
{t('Enter to select, ↑↓ to navigate, Esc to close')}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
|
|
|||
62
packages/cli/src/ui/components/Tips.test.ts
Normal file
62
packages/cli/src/ui/components/Tips.test.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { selectWeightedTip } from './Tips.js';
|
||||
|
||||
describe('selectWeightedTip', () => {
|
||||
const tips = [
|
||||
{ text: 'tip-a', weight: 1 },
|
||||
{ text: 'tip-b', weight: 3 },
|
||||
{ text: 'tip-c', weight: 1 },
|
||||
];
|
||||
|
||||
it('returns a valid tip text', () => {
|
||||
const result = selectWeightedTip(tips);
|
||||
expect(['tip-a', 'tip-b', 'tip-c']).toContain(result);
|
||||
});
|
||||
|
||||
it('selects the first tip when random is near zero', () => {
|
||||
vi.spyOn(Math, 'random').mockReturnValue(0);
|
||||
expect(selectWeightedTip(tips)).toBe('tip-a');
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('selects the weighted tip when random falls in its range', () => {
|
||||
// Total weight = 5. tip-a covers [0,1), tip-b covers [1,4), tip-c covers [4,5)
|
||||
// Math.random() * 5 = 2.0 falls in tip-b's range
|
||||
vi.spyOn(Math, 'random').mockReturnValue(0.4); // 0.4 * 5 = 2.0
|
||||
expect(selectWeightedTip(tips)).toBe('tip-b');
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('selects the last tip when random is near max', () => {
|
||||
vi.spyOn(Math, 'random').mockReturnValue(0.99);
|
||||
expect(selectWeightedTip(tips)).toBe('tip-c');
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('respects weight distribution over many samples', () => {
|
||||
const counts: Record<string, number> = {
|
||||
'tip-a': 0,
|
||||
'tip-b': 0,
|
||||
'tip-c': 0,
|
||||
};
|
||||
const iterations = 10000;
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const result = selectWeightedTip(tips);
|
||||
counts[result]!++;
|
||||
}
|
||||
// tip-b (weight 3) should appear roughly 3x as often as tip-a or tip-c (weight 1)
|
||||
// With 10k iterations, we expect: tip-a ~2000, tip-b ~6000, tip-c ~2000
|
||||
expect(counts['tip-b']!).toBeGreaterThan(counts['tip-a']! * 2);
|
||||
expect(counts['tip-b']!).toBeGreaterThan(counts['tip-c']! * 2);
|
||||
});
|
||||
|
||||
it('handles single tip', () => {
|
||||
expect(selectWeightedTip([{ text: 'only', weight: 1 }])).toBe('only');
|
||||
});
|
||||
});
|
||||
|
|
@ -9,7 +9,9 @@ import { Box, Text } from 'ink';
|
|||
import { theme } from '../semantic-colors.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
const startupTips = [
|
||||
type Tip = string | { text: string; weight: number };
|
||||
|
||||
const startupTips: Tip[] = [
|
||||
'Use /compress when the conversation gets long to summarize history and free up context.',
|
||||
'Start a fresh idea with /clear or /new; the previous session stays available in history.',
|
||||
'Use /bug to submit issues to the maintainers when something goes off.',
|
||||
|
|
@ -20,13 +22,34 @@ const startupTips = [
|
|||
process.platform === 'win32'
|
||||
? 'You can switch permission mode quickly with Tab or /approval-mode.'
|
||||
: 'You can switch permission mode quickly with Shift+Tab or /approval-mode.',
|
||||
] as const;
|
||||
{
|
||||
text: 'Try /insight to generate personalized insights from your chat history.',
|
||||
weight: 3,
|
||||
},
|
||||
];
|
||||
|
||||
function tipText(tip: Tip): string {
|
||||
return typeof tip === 'string' ? tip : tip.text;
|
||||
}
|
||||
|
||||
function tipWeight(tip: Tip): number {
|
||||
return typeof tip === 'string' ? 1 : tip.weight;
|
||||
}
|
||||
|
||||
export function selectWeightedTip(tips: Tip[]): string {
|
||||
const totalWeight = tips.reduce((sum, tip) => sum + tipWeight(tip), 0);
|
||||
let random = Math.random() * totalWeight;
|
||||
for (const tip of tips) {
|
||||
random -= tipWeight(tip);
|
||||
if (random <= 0) {
|
||||
return tipText(tip);
|
||||
}
|
||||
}
|
||||
return tipText(tips[tips.length - 1]!);
|
||||
}
|
||||
|
||||
export const Tips: React.FC = () => {
|
||||
const selectedTip = useMemo(() => {
|
||||
const randomIndex = Math.floor(Math.random() * startupTips.length);
|
||||
return startupTips[randomIndex];
|
||||
}, []);
|
||||
const selectedTip = useMemo(() => selectWeightedTip(startupTips), []);
|
||||
|
||||
return (
|
||||
<Box marginLeft={2} marginRight={2}>
|
||||
|
|
|
|||
|
|
@ -10,9 +10,17 @@ import { theme } from '../../semantic-colors.js';
|
|||
|
||||
interface ErrorMessageProps {
|
||||
text: string;
|
||||
/** Optional inline hint displayed after the error text in secondary/dimmed color */
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
export const ErrorMessage: React.FC<ErrorMessageProps> = ({ text }) => {
|
||||
/**
|
||||
* Renders an error message with a "✕" prefix.
|
||||
* When a hint is provided (e.g., retry countdown), it is displayed inline
|
||||
* in parentheses with a dimmed secondary color, similar to the ESC hint
|
||||
* style used in LoadingIndicator.
|
||||
*/
|
||||
export const ErrorMessage: React.FC<ErrorMessageProps> = ({ text, hint }) => {
|
||||
const prefix = '✕ ';
|
||||
const prefixWidth = prefix.length;
|
||||
|
||||
|
|
@ -21,10 +29,9 @@ export const ErrorMessage: React.FC<ErrorMessageProps> = ({ text }) => {
|
|||
<Box width={prefixWidth}>
|
||||
<Text color={theme.status.error}>{prefix}</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
<Text wrap="wrap" color={theme.status.error}>
|
||||
{text}
|
||||
</Text>
|
||||
<Box flexGrow={1} flexWrap="wrap" flexDirection="row">
|
||||
<Text color={theme.status.error}>{text}</Text>
|
||||
{hint && <Text color={theme.text.secondary}> ({hint})</Text>}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ export interface BaseSelectionListProps<
|
|||
showNumbers?: boolean;
|
||||
showScrollArrows?: boolean;
|
||||
maxItemsToShow?: number;
|
||||
/** Gap (in rows) between each item. */
|
||||
itemGap?: number;
|
||||
renderItem: (item: TItem, context: RenderItemContext) => React.ReactNode;
|
||||
}
|
||||
|
||||
|
|
@ -59,6 +61,7 @@ export function BaseSelectionList<
|
|||
showNumbers = true,
|
||||
showScrollArrows = false,
|
||||
maxItemsToShow = 10,
|
||||
itemGap = 0,
|
||||
renderItem,
|
||||
}: BaseSelectionListProps<T, TItem>): React.JSX.Element {
|
||||
const { activeIndex } = useSelectionList({
|
||||
|
|
@ -89,7 +92,7 @@ export function BaseSelectionList<
|
|||
const numberColumnWidth = String(items.length).length;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box flexDirection="column" gap={itemGap}>
|
||||
{/* Use conditional coloring instead of conditional rendering */}
|
||||
{showScrollArrows && (
|
||||
<Text
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import type { SelectionListItem } from '../../hooks/useSelectionList.js';
|
|||
|
||||
export interface DescriptiveRadioSelectItem<T> extends SelectionListItem<T> {
|
||||
title: React.ReactNode;
|
||||
description: string;
|
||||
description: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface DescriptiveRadioButtonSelectProps<T> {
|
||||
|
|
@ -32,6 +32,8 @@ export interface DescriptiveRadioButtonSelectProps<T> {
|
|||
showScrollArrows?: boolean;
|
||||
/** The maximum number of items to show at once. */
|
||||
maxItemsToShow?: number;
|
||||
/** Gap (in rows) between each item. */
|
||||
itemGap?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -48,6 +50,7 @@ export function DescriptiveRadioButtonSelect<T>({
|
|||
showNumbers = false,
|
||||
showScrollArrows = false,
|
||||
maxItemsToShow = 10,
|
||||
itemGap = 0,
|
||||
}: DescriptiveRadioButtonSelectProps<T>): React.JSX.Element {
|
||||
return (
|
||||
<BaseSelectionList<T, DescriptiveRadioSelectItem<T>>
|
||||
|
|
@ -59,6 +62,7 @@ export function DescriptiveRadioButtonSelect<T>({
|
|||
showNumbers={showNumbers}
|
||||
showScrollArrows={showScrollArrows}
|
||||
maxItemsToShow={maxItemsToShow}
|
||||
itemGap={itemGap}
|
||||
renderItem={(item, { titleColor }) => (
|
||||
<Box flexDirection="column" key={item.key}>
|
||||
<Text color={titleColor}>{item.title}</Text>
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ export interface UIActions {
|
|||
onSuggestionsVisibilityChange: (visible: boolean) => void;
|
||||
refreshStatic: () => void;
|
||||
handleFinalSubmit: (value: string) => void;
|
||||
handleRetryLastPrompt: () => void;
|
||||
handleClearScreen: () => void;
|
||||
// Welcome back dialog
|
||||
handleWelcomeBackSelection: (choice: 'continue' | 'restart') => void;
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ describe('useCodingPlanUpdates', () => {
|
|||
|
||||
// Should prompt for China region since it defaults to China
|
||||
expect(result.current.codingPlanUpdateRequest?.prompt).toContain(
|
||||
chinaConfig.regionName,
|
||||
'Alibaba Cloud Coding Plan',
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -135,7 +135,7 @@ describe('useCodingPlanUpdates', () => {
|
|||
});
|
||||
|
||||
expect(result.current.codingPlanUpdateRequest?.prompt).toContain(
|
||||
chinaConfig.regionName,
|
||||
'Alibaba Cloud Coding Plan',
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -158,7 +158,7 @@ describe('useCodingPlanUpdates', () => {
|
|||
});
|
||||
|
||||
expect(result.current.codingPlanUpdateRequest?.prompt).toContain(
|
||||
globalConfig.regionName,
|
||||
'Alibaba Cloud Coding Plan',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -228,7 +228,7 @@ describe('useCodingPlanUpdates', () => {
|
|||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'info',
|
||||
text: expect.stringContaining(chinaConfig.regionName),
|
||||
text: expect.stringContaining('Alibaba Cloud Coding Plan'),
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
|
|
@ -297,7 +297,7 @@ describe('useCodingPlanUpdates', () => {
|
|||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'info',
|
||||
text: expect.stringContaining(globalConfig.regionName),
|
||||
text: expect.stringContaining('Alibaba Cloud Coding Plan'),
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ export function useCodingPlanUpdates(
|
|||
);
|
||||
|
||||
// Get the configuration for the current region
|
||||
const { template, version, regionName } = getCodingPlanConfig(region);
|
||||
const { template, version } = getCodingPlanConfig(region);
|
||||
|
||||
// Generate new configs from template
|
||||
const newConfigs = template.map((templateConfig) => ({
|
||||
|
|
@ -117,7 +117,7 @@ export function useCodingPlanUpdates(
|
|||
type: 'info',
|
||||
text: t(
|
||||
'{{region}} configuration updated successfully. Model switched to "{{model}}".',
|
||||
{ region: regionName, model: activeModel },
|
||||
{ region: t('Alibaba Cloud Coding Plan'), model: activeModel },
|
||||
),
|
||||
},
|
||||
Date.now(),
|
||||
|
|
@ -170,11 +170,10 @@ export function useCodingPlanUpdates(
|
|||
|
||||
// Check if version matches
|
||||
if (savedVersion !== currentVersion) {
|
||||
const { regionName } = getCodingPlanConfig(region);
|
||||
setUpdateRequest({
|
||||
prompt: t(
|
||||
'New model configurations are available for {{region}}. Update now?',
|
||||
{ region: regionName },
|
||||
{ region: t('Alibaba Cloud Coding Plan') },
|
||||
),
|
||||
onConfirm: async (confirmed: boolean) => {
|
||||
setUpdateRequest(undefined);
|
||||
|
|
|
|||
|
|
@ -2304,40 +2304,30 @@ describe('useGeminiStream', () => {
|
|||
result.current.pendingHistoryItems.find(
|
||||
(item) => item.type === MessageType.ERROR,
|
||||
);
|
||||
const findCountdownItem = () =>
|
||||
result.current.pendingHistoryItems.find(
|
||||
(item) => item.type === 'retry_countdown',
|
||||
);
|
||||
|
||||
let errorItem = findErrorItem();
|
||||
let countdownItem = findCountdownItem();
|
||||
for (
|
||||
let attempts = 0;
|
||||
attempts < 5 && (!errorItem || !countdownItem);
|
||||
attempts++
|
||||
) {
|
||||
for (let attempts = 0; attempts < 5 && !errorItem; attempts++) {
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
errorItem = findErrorItem();
|
||||
countdownItem = findCountdownItem();
|
||||
}
|
||||
|
||||
// Error line should be rendered as ERROR type (wrapped by parseAndFormatApiError)
|
||||
// Error item should contain the error text and a retry hint
|
||||
expect(errorItem?.text).toContain('Rate limit exceeded');
|
||||
|
||||
// Countdown line should be rendered as retry_countdown type
|
||||
expect(countdownItem?.text).toContain('Retrying in 3 seconds');
|
||||
// Countdown hint should be inline on the error item (not a separate item)
|
||||
expect((errorItem as { hint?: string })?.hint).toContain('3s');
|
||||
expect((errorItem as { hint?: string })?.hint).toContain('attempt 1/3');
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
});
|
||||
|
||||
const countdownAfterOneSecond = result.current.pendingHistoryItems.find(
|
||||
(item) => item.type === 'retry_countdown',
|
||||
const errorAfterOneSecond = result.current.pendingHistoryItems.find(
|
||||
(item) => item.type === MessageType.ERROR,
|
||||
);
|
||||
expect(countdownAfterOneSecond?.text).toContain(
|
||||
'Retrying in 2 seconds',
|
||||
expect((errorAfterOneSecond as { hint?: string })?.hint).toContain(
|
||||
'2s',
|
||||
);
|
||||
|
||||
resolveStream?.();
|
||||
|
|
@ -2347,15 +2337,11 @@ describe('useGeminiStream', () => {
|
|||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
// Both error and countdown should be cleared after retry succeeds
|
||||
// Error item (with hint) should be cleared after retry succeeds
|
||||
const remainingError = result.current.pendingHistoryItems.find(
|
||||
(item) => item.type === MessageType.ERROR,
|
||||
);
|
||||
const remainingCountdown = result.current.pendingHistoryItems.find(
|
||||
(item) => item.type === 'retry_countdown',
|
||||
);
|
||||
expect(remainingError).toBeUndefined();
|
||||
expect(remainingCountdown).toBeUndefined();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
|
|
@ -2525,14 +2511,13 @@ describe('useGeminiStream', () => {
|
|||
await result.current.submitQuery('Test query');
|
||||
});
|
||||
|
||||
// Verify error message was added
|
||||
// Verify error message appears in pending history items (not via addItem,
|
||||
// since errors with retry hints are now stored as pending items)
|
||||
await waitFor(() => {
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'error',
|
||||
}),
|
||||
expect.any(Number),
|
||||
const errorItem = result.current.pendingHistoryItems.find(
|
||||
(item) => item.type === 'error',
|
||||
);
|
||||
expect(errorItem).toBeDefined();
|
||||
});
|
||||
|
||||
// Verify parseAndFormatApiError was called
|
||||
|
|
|
|||
|
|
@ -169,12 +169,17 @@ export const useGeminiStream = (
|
|||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const turnCancelledRef = useRef(false);
|
||||
const isSubmittingQueryRef = useRef(false);
|
||||
const lastPromptRef = useRef<PartListUnion | null>(null);
|
||||
const lastPromptErroredRef = useRef(false);
|
||||
const [isResponding, setIsResponding] = useState<boolean>(false);
|
||||
const [thought, setThought] = useState<ThoughtSummary | null>(null);
|
||||
const [pendingHistoryItem, pendingHistoryItemRef, setPendingHistoryItem] =
|
||||
useStateAndRef<HistoryItemWithoutId | null>(null);
|
||||
const [pendingRetryErrorItem, setPendingRetryErrorItem] =
|
||||
useState<HistoryItemWithoutId | null>(null);
|
||||
const [
|
||||
pendingRetryErrorItem,
|
||||
pendingRetryErrorItemRef,
|
||||
setPendingRetryErrorItem,
|
||||
] = useStateAndRef<HistoryItemWithoutId | null>(null);
|
||||
const [
|
||||
pendingRetryCountdownItem,
|
||||
pendingRetryCountdownItemRef,
|
||||
|
|
@ -254,11 +259,18 @@ export const useGeminiStream = (
|
|||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Clears the retry countdown timer and pending retry items.
|
||||
*/
|
||||
const clearRetryCountdown = useCallback(() => {
|
||||
stopRetryCountdownTimer();
|
||||
setPendingRetryErrorItem(null);
|
||||
setPendingRetryCountdownItem(null);
|
||||
}, [setPendingRetryCountdownItem, stopRetryCountdownTimer]);
|
||||
}, [
|
||||
setPendingRetryErrorItem,
|
||||
setPendingRetryCountdownItem,
|
||||
stopRetryCountdownTimer,
|
||||
]);
|
||||
|
||||
const startRetryCountdown = useCallback(
|
||||
(retryInfo: {
|
||||
|
|
@ -273,18 +285,21 @@ export const useGeminiStream = (
|
|||
const retryReasonText =
|
||||
message ?? t('Rate limit exceeded. Please wait and try again.');
|
||||
|
||||
// Error line stays static (red with ✕ prefix)
|
||||
setPendingRetryErrorItem({
|
||||
type: MessageType.ERROR,
|
||||
text: retryReasonText,
|
||||
});
|
||||
|
||||
// Countdown line updates every second (dim/secondary color)
|
||||
const updateCountdown = () => {
|
||||
const elapsedMs = Date.now() - startTime;
|
||||
const remainingMs = Math.max(0, delayMs - elapsedMs);
|
||||
const remainingSec = Math.ceil(remainingMs / 1000);
|
||||
|
||||
// Update error item with hint containing countdown info (short format)
|
||||
const hintText = `Retrying in ${remainingSec}s… (attempt ${attempt}/${maxRetries})`;
|
||||
|
||||
setPendingRetryErrorItem({
|
||||
type: MessageType.ERROR,
|
||||
text: retryReasonText,
|
||||
hint: hintText,
|
||||
});
|
||||
|
||||
setPendingRetryCountdownItem({
|
||||
type: 'retry_countdown',
|
||||
text: t(
|
||||
|
|
@ -305,7 +320,11 @@ export const useGeminiStream = (
|
|||
updateCountdown();
|
||||
retryCountdownTimerRef.current = setInterval(updateCountdown, 1000);
|
||||
},
|
||||
[setPendingRetryCountdownItem, stopRetryCountdownTimer],
|
||||
[
|
||||
setPendingRetryErrorItem,
|
||||
setPendingRetryCountdownItem,
|
||||
stopRetryCountdownTimer,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => () => stopRetryCountdownTimer(), [stopRetryCountdownTimer]);
|
||||
|
|
@ -693,6 +712,7 @@ export const useGeminiStream = (
|
|||
return;
|
||||
}
|
||||
|
||||
lastPromptErroredRef.current = false;
|
||||
if (pendingHistoryItemRef.current) {
|
||||
if (pendingHistoryItemRef.current.type === 'tool_group') {
|
||||
const updatedTools = pendingHistoryItemRef.current.tools.map(
|
||||
|
|
@ -732,27 +752,36 @@ export const useGeminiStream = (
|
|||
|
||||
const handleErrorEvent = useCallback(
|
||||
(eventValue: GeminiErrorEventValue, userMessageTimestamp: number) => {
|
||||
lastPromptErroredRef.current = true;
|
||||
if (pendingHistoryItemRef.current) {
|
||||
addItem(pendingHistoryItemRef.current, userMessageTimestamp);
|
||||
setPendingHistoryItem(null);
|
||||
}
|
||||
addItem(
|
||||
{
|
||||
type: MessageType.ERROR,
|
||||
// Only show Ctrl+Y hint if not already showing an auto-retry countdown
|
||||
// (auto-retry countdown is shown when retryCountdownTimerRef is active)
|
||||
const isShowingAutoRetry = retryCountdownTimerRef.current !== null;
|
||||
clearRetryCountdown();
|
||||
if (!isShowingAutoRetry) {
|
||||
const retryHint = t('Press Ctrl+Y to retry');
|
||||
// Store error with hint as a pending item (not in history).
|
||||
// This allows the hint to be removed when the user retries with Ctrl+Y,
|
||||
// since pending items are in the dynamic rendering area (not <Static>).
|
||||
setPendingRetryErrorItem({
|
||||
type: 'error' as const,
|
||||
text: parseAndFormatApiError(
|
||||
eventValue.error,
|
||||
config.getContentGeneratorConfig()?.authType,
|
||||
),
|
||||
},
|
||||
userMessageTimestamp,
|
||||
);
|
||||
clearRetryCountdown();
|
||||
hint: retryHint,
|
||||
});
|
||||
}
|
||||
setThought(null); // Reset thought when there's an error
|
||||
},
|
||||
[
|
||||
addItem,
|
||||
pendingHistoryItemRef,
|
||||
setPendingHistoryItem,
|
||||
setPendingRetryErrorItem,
|
||||
config,
|
||||
setThought,
|
||||
clearRetryCountdown,
|
||||
|
|
@ -816,7 +845,10 @@ export const useGeminiStream = (
|
|||
userMessageTimestamp,
|
||||
);
|
||||
}
|
||||
clearRetryCountdown();
|
||||
// Only clear auto-retry countdown errors (those with active timer)
|
||||
if (retryCountdownTimerRef.current) {
|
||||
clearRetryCountdown();
|
||||
}
|
||||
},
|
||||
[addItem, clearRetryCountdown],
|
||||
);
|
||||
|
|
@ -1032,7 +1064,7 @@ export const useGeminiStream = (
|
|||
const submitQuery = useCallback(
|
||||
async (
|
||||
query: PartListUnion,
|
||||
options?: { isContinuation: boolean },
|
||||
options?: { isContinuation: boolean; skipPreparation?: boolean },
|
||||
prompt_id?: string,
|
||||
) => {
|
||||
// Prevent concurrent executions of submitQuery, but allow continuations
|
||||
|
|
@ -1056,7 +1088,11 @@ export const useGeminiStream = (
|
|||
// Reset quota error flag when starting a new query (not a continuation)
|
||||
if (!options?.isContinuation) {
|
||||
setModelSwitchedFromQuotaError(false);
|
||||
// No quota-error / fallback routing mechanism currently; keep state minimal.
|
||||
// Commit any pending retry error to history (without hint) since the
|
||||
// user is starting a new conversation turn
|
||||
if (pendingRetryCountdownItemRef.current) {
|
||||
clearRetryCountdown();
|
||||
}
|
||||
}
|
||||
|
||||
abortControllerRef.current = new AbortController();
|
||||
|
|
@ -1068,12 +1104,14 @@ export const useGeminiStream = (
|
|||
}
|
||||
|
||||
return promptIdContext.run(prompt_id, async () => {
|
||||
const { queryToSend, shouldProceed } = await prepareQueryForGemini(
|
||||
query,
|
||||
userMessageTimestamp,
|
||||
abortSignal,
|
||||
prompt_id!,
|
||||
);
|
||||
const { queryToSend, shouldProceed } = options?.skipPreparation
|
||||
? { queryToSend: query, shouldProceed: true }
|
||||
: await prepareQueryForGemini(
|
||||
query,
|
||||
userMessageTimestamp,
|
||||
abortSignal,
|
||||
prompt_id!,
|
||||
);
|
||||
|
||||
if (!shouldProceed || queryToSend === null) {
|
||||
isSubmittingQueryRef.current = false;
|
||||
|
|
@ -1095,6 +1133,8 @@ export const useGeminiStream = (
|
|||
}
|
||||
|
||||
const finalQueryToSend = queryToSend;
|
||||
lastPromptRef.current = finalQueryToSend;
|
||||
lastPromptErroredRef.current = false;
|
||||
|
||||
if (!options?.isContinuation) {
|
||||
// trigger new prompt event for session stats in CLI
|
||||
|
|
@ -1143,6 +1183,12 @@ export const useGeminiStream = (
|
|||
addItem(pendingHistoryItemRef.current, userMessageTimestamp);
|
||||
setPendingHistoryItem(null);
|
||||
}
|
||||
// Only clear auto-retry countdown errors (those with an active timer).
|
||||
// Do NOT clear static error+hint from handleErrorEvent — those should
|
||||
// remain visible until the user presses Ctrl+Y to retry.
|
||||
if (retryCountdownTimerRef.current) {
|
||||
clearRetryCountdown();
|
||||
}
|
||||
if (loopDetectedRef.current) {
|
||||
loopDetectedRef.current = false;
|
||||
handleLoopDetectedEvent();
|
||||
|
|
@ -1151,16 +1197,17 @@ export const useGeminiStream = (
|
|||
if (error instanceof UnauthorizedError) {
|
||||
onAuthError('Session expired or is unauthorized.');
|
||||
} else if (!isNodeError(error) || error.name !== 'AbortError') {
|
||||
addItem(
|
||||
{
|
||||
type: MessageType.ERROR,
|
||||
text: parseAndFormatApiError(
|
||||
getErrorMessage(error) || 'Unknown error',
|
||||
config.getContentGeneratorConfig()?.authType,
|
||||
),
|
||||
},
|
||||
userMessageTimestamp,
|
||||
);
|
||||
lastPromptErroredRef.current = true;
|
||||
const retryHint = t('Press Ctrl+Y to retry');
|
||||
// Store error with hint as a pending item (same as handleErrorEvent)
|
||||
setPendingRetryErrorItem({
|
||||
type: 'error' as const,
|
||||
text: parseAndFormatApiError(
|
||||
getErrorMessage(error) || 'Unknown error',
|
||||
config.getContentGeneratorConfig()?.authType,
|
||||
),
|
||||
hint: retryHint,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setIsResponding(false);
|
||||
|
|
@ -1183,9 +1230,71 @@ export const useGeminiStream = (
|
|||
startNewPrompt,
|
||||
getPromptCount,
|
||||
handleLoopDetectedEvent,
|
||||
clearRetryCountdown,
|
||||
pendingRetryCountdownItemRef,
|
||||
setPendingRetryErrorItem,
|
||||
],
|
||||
);
|
||||
|
||||
/**
|
||||
* Retries the last failed prompt when the user presses Ctrl+Y.
|
||||
*
|
||||
* Activation conditions for Ctrl+Y shortcut:
|
||||
* 1. ✅ The last request must have failed (lastPromptErroredRef.current === true)
|
||||
* 2. ✅ Current streaming state must NOT be "Responding" (avoid interrupting ongoing stream)
|
||||
* 3. ✅ Current streaming state must NOT be "WaitingForConfirmation" (avoid conflicting with tool confirmation flow)
|
||||
* 4. ✅ There must be a stored lastPrompt in lastPromptRef.current
|
||||
*
|
||||
* When conditions are not met:
|
||||
* - If streaming is active (Responding/WaitingForConfirmation): silently return without action
|
||||
* - If no failed request exists: display "No failed request to retry." info message
|
||||
*
|
||||
* When conditions are met:
|
||||
* - Clears any pending auto-retry countdown to avoid duplicate retries
|
||||
* - Re-submits the last query with skipPreparation: true for faster retry
|
||||
*
|
||||
* This function is exposed via UIActionsContext and triggered by InputPrompt
|
||||
* when the user presses Ctrl+Y (bound to Command.RETRY_LAST in keyBindings.ts).
|
||||
*/
|
||||
const retryLastPrompt = useCallback(async () => {
|
||||
if (
|
||||
streamingState === StreamingState.Responding ||
|
||||
streamingState === StreamingState.WaitingForConfirmation
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastPrompt = lastPromptRef.current;
|
||||
if (!lastPrompt || !lastPromptErroredRef.current) {
|
||||
addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: t('No failed request to retry.'),
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Commit the error to history (without hint) before clearing
|
||||
const errorItem = pendingRetryErrorItemRef.current;
|
||||
if (errorItem) {
|
||||
addItem({ type: errorItem.type, text: errorItem.text }, Date.now());
|
||||
}
|
||||
clearRetryCountdown();
|
||||
|
||||
await submitQuery(lastPrompt, {
|
||||
isContinuation: false,
|
||||
skipPreparation: true,
|
||||
});
|
||||
}, [
|
||||
streamingState,
|
||||
addItem,
|
||||
clearRetryCountdown,
|
||||
submitQuery,
|
||||
pendingRetryErrorItemRef,
|
||||
]);
|
||||
|
||||
const handleApprovalModeChange = useCallback(
|
||||
async (newApprovalMode: ApprovalMode) => {
|
||||
// Auto-approve pending tool calls when switching to auto-approval modes
|
||||
|
|
@ -1489,6 +1598,7 @@ export const useGeminiStream = (
|
|||
pendingHistoryItems,
|
||||
thought,
|
||||
cancelOngoingRequest,
|
||||
retryLastPrompt,
|
||||
pendingToolCalls: toolCalls,
|
||||
handleApprovalModeChange,
|
||||
activePtyId,
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ describe('keyMatchers', () => {
|
|||
[Command.QUIT]: (key: Key) => key.ctrl && key.name === 'c',
|
||||
[Command.EXIT]: (key: Key) => key.ctrl && key.name === 'd',
|
||||
[Command.SHOW_MORE_LINES]: (key: Key) => key.ctrl && key.name === 's',
|
||||
[Command.RETRY_LAST]: (key: Key) => key.ctrl && key.name === 'y',
|
||||
[Command.REVERSE_SEARCH]: (key: Key) => key.ctrl && key.name === 'r',
|
||||
[Command.SUBMIT_REVERSE_SEARCH]: (key: Key) =>
|
||||
key.name === 'return' && !key.ctrl,
|
||||
|
|
@ -252,6 +253,11 @@ describe('keyMatchers', () => {
|
|||
positive: [createKey('s', { ctrl: true })],
|
||||
negative: [createKey('s'), createKey('l', { ctrl: true })],
|
||||
},
|
||||
{
|
||||
command: Command.RETRY_LAST,
|
||||
positive: [createKey('y', { ctrl: true })],
|
||||
negative: [createKey('y'), createKey('r', { ctrl: true })],
|
||||
},
|
||||
|
||||
// Shell commands
|
||||
{
|
||||
|
|
|
|||
|
|
@ -121,6 +121,7 @@ export type HistoryItemInfo = HistoryItemBase & {
|
|||
export type HistoryItemError = HistoryItemBase & {
|
||||
type: 'error';
|
||||
text: string;
|
||||
hint?: string; // Optional inline hint (e.g., retry countdown) displayed in secondary color
|
||||
};
|
||||
|
||||
export type HistoryItemWarning = HistoryItemBase & {
|
||||
|
|
|
|||
|
|
@ -380,4 +380,62 @@ describe('languageUtils', () => {
|
|||
expect(fs.writeFileSync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('output-language.md path resolution priority', () => {
|
||||
it('should prefer project-level path over global path', () => {
|
||||
const projectPath = '/project/.qwen/output-language.md';
|
||||
const globalPath = '/mock/home/.qwen/output-language.md';
|
||||
|
||||
vi.mocked(fs.existsSync).mockImplementation((p) => {
|
||||
if (p.toString() === projectPath) return true;
|
||||
if (p.toString() === globalPath) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
let resolvedPath: string | undefined;
|
||||
if (fs.existsSync(projectPath)) {
|
||||
resolvedPath = projectPath;
|
||||
} else if (fs.existsSync(globalPath)) {
|
||||
resolvedPath = globalPath;
|
||||
}
|
||||
|
||||
expect(resolvedPath).toBe(projectPath);
|
||||
});
|
||||
|
||||
it('should fall back to global path when project-level does not exist', () => {
|
||||
const projectPath = '/project/.qwen/output-language.md';
|
||||
const globalPath = '/mock/home/.qwen/output-language.md';
|
||||
|
||||
vi.mocked(fs.existsSync).mockImplementation((p) => {
|
||||
if (p.toString() === projectPath) return false;
|
||||
if (p.toString() === globalPath) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
let resolvedPath: string | undefined;
|
||||
if (fs.existsSync(projectPath)) {
|
||||
resolvedPath = projectPath;
|
||||
} else if (fs.existsSync(globalPath)) {
|
||||
resolvedPath = globalPath;
|
||||
}
|
||||
|
||||
expect(resolvedPath).toBe(globalPath);
|
||||
});
|
||||
|
||||
it('should return undefined when neither path exists', () => {
|
||||
const projectPath = '/project/.qwen/output-language.md';
|
||||
const globalPath = '/mock/home/.qwen/output-language.md';
|
||||
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
|
||||
let resolvedPath: string | undefined;
|
||||
if (fs.existsSync(projectPath)) {
|
||||
resolvedPath = projectPath;
|
||||
} else if (fs.existsSync(globalPath)) {
|
||||
resolvedPath = globalPath;
|
||||
}
|
||||
|
||||
expect(resolvedPath).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue