Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 56 additions & 8 deletions packages/acp-bridge/src/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3143,6 +3143,58 @@ export function createAcpSessionBridge(opts: BridgeOptions): AcpSessionBridge {
return response;
},

async setSessionLanguage(sessionId, params, context) {
Comment thread
chiga0 marked this conversation as resolved.
Comment thread
chiga0 marked this conversation as resolved.
const entry = byId.get(sessionId);
if (!entry) throw new SessionNotFoundError(sessionId);
const info = channelInfoForEntry(entry);
if (!info || info.isDying) throw new SessionNotFoundError(sessionId);
const originatorClientId = resolveTrustedClientId(
entry,
context?.clientId,
);

const result = (await Promise.race([
withTimeout(
Comment thread
chiga0 marked this conversation as resolved.
entry.connection.extMethod(
SERVE_CONTROL_EXT_METHODS.sessionLanguage,
{
sessionId,
language: params.language,
syncOutputLanguage: params.syncOutputLanguage,
},
),
initTimeoutMs,
Comment thread
chiga0 marked this conversation as resolved.
SERVE_CONTROL_EXT_METHODS.sessionLanguage,
),
Comment thread
chiga0 marked this conversation as resolved.
Comment thread
chiga0 marked this conversation as resolved.
Comment thread
chiga0 marked this conversation as resolved.
getTransportClosedReject(entry),
])) as {
language: string;
outputLanguage: string | null;
refreshed: boolean;
};

try {
Comment thread
chiga0 marked this conversation as resolved.
entry.events.publish({
type: 'language_changed',
Comment thread
chiga0 marked this conversation as resolved.
data: {
sessionId: entry.sessionId,
language: result.language,
Comment thread
chiga0 marked this conversation as resolved.
outputLanguage: result.outputLanguage ?? null,
refreshed: result.refreshed ?? false,
},
...(originatorClientId ? { originatorClientId } : {}),
});
Comment thread
chiga0 marked this conversation as resolved.
} catch {
/* bus closed */
}

return {
Comment thread
chiga0 marked this conversation as resolved.
language: result.language,
outputLanguage: result.outputLanguage ?? null,
refreshed: result.refreshed ?? false,
};
},
Comment thread
chiga0 marked this conversation as resolved.

async setSessionApprovalMode(sessionId, mode, opts, context) {
// Forwards through `qwen/control/session/approval_mode` so the
// change lands inside the ACP child's own `Config` (per-session
Expand Down Expand Up @@ -3532,8 +3584,7 @@ export function createAcpSessionBridge(opts: BridgeOptions): AcpSessionBridge {
const data = (err as { data?: unknown })?.data;
if (data && typeof data === 'object' && 'errorKind' in data) {
const kind = (data as { errorKind: string }).errorKind;
const msg =
(err as { message?: string })?.message ?? 'Rewind failed';
const msg = (err as { message?: string })?.message ?? 'Rewind failed';
if (kind === 'session_busy') {
throw new SessionBusyError(sessionId, msg);
}
Expand All @@ -3544,12 +3595,9 @@ export function createAcpSessionBridge(opts: BridgeOptions): AcpSessionBridge {
throw err;
}

const targetTurnIndex =
(response['targetTurnIndex'] as number) ?? 0;
const filesChanged =
(response['filesChanged'] as string[]) ?? [];
const filesFailed =
(response['filesFailed'] as string[]) ?? [];
const targetTurnIndex = (response['targetTurnIndex'] as number) ?? 0;
const filesChanged = (response['filesChanged'] as string[]) ?? [];
const filesFailed = (response['filesFailed'] as string[]) ?? [];

try {
entry.events.publish({
Expand Down
16 changes: 16 additions & 0 deletions packages/acp-bridge/src/bridgeTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,22 @@ export interface AcpSessionBridge {
context?: BridgeClientRequestContext,
): Promise<SetSessionModelResponse>;

/**
* Switch UI language and optionally LLM output language for a live
* session, then broadcast a `language_changed` event. When
* `syncOutputLanguage` is true the handler also refreshes every
Comment thread
chiga0 marked this conversation as resolved.
* session's system prompt so the next LLM call uses the new language.
*/
setSessionLanguage(
sessionId: string,
params: { language: string; syncOutputLanguage: boolean },
context?: BridgeClientRequestContext,
Comment thread
chiga0 marked this conversation as resolved.
): Promise<{
language: string;
outputLanguage: string | null;
refreshed: boolean;
}>;

/**
* Change the approval mode of a live session and broadcast an
* `approval_mode_changed` event. `opts.persist === true` also writes
Expand Down
1 change: 1 addition & 0 deletions packages/acp-bridge/src/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export const SERVE_CONTROL_EXT_METHODS = {
sessionRecap: 'qwen/control/session/recap',
sessionBtw: 'qwen/control/session/btw',
sessionShellHistory: 'qwen/control/session/shell_history',
sessionLanguage: 'qwen/control/session/language',
sessionRewind: 'qwen/control/session/rewind',
workspaceMcpRestart: 'qwen/control/workspace/mcp/restart',
workspaceMcpManage: 'qwen/control/workspace/mcp/manage',
Expand Down
107 changes: 107 additions & 0 deletions packages/cli/src/acp-integration/acpAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,17 @@ import {
import { runWithAcpRuntimeOutputDir } from './runtimeOutputDirContext.js';
import { runExitCleanup } from '../utils/cleanup.js';
import { appEvents, AppEvent } from '../utils/events.js';
import {
setLanguageAsync,
getCurrentLanguage,
SUPPORTED_LANGUAGES,
} from '../i18n/index.js';
import {
resolveOutputLanguage,
updateOutputLanguageFile,
isAutoLanguage,
OUTPUT_LANGUAGE_AUTO,
} from '../utils/languageUtils.js';
import {
ACP_PREFLIGHT_KINDS,
STATUS_SCHEMA_VERSION,
Expand Down Expand Up @@ -2739,6 +2750,102 @@ class QwenAgent implements Agent {
const current = config.getApprovalMode();
return { previous, current };
}
case SERVE_CONTROL_EXT_METHODS.sessionLanguage: {
Comment thread
chiga0 marked this conversation as resolved.
const sessionId = params['sessionId'];
const language = params['language'];
const syncOutputLanguage = params['syncOutputLanguage'] === true;

if (typeof sessionId !== 'string' || sessionId.length === 0) {
throw RequestError.invalidParams(
undefined,
'Invalid or missing sessionId',
);
}
const allowedLanguages = [
Comment thread
chiga0 marked this conversation as resolved.
Comment thread
chiga0 marked this conversation as resolved.
Comment thread
chiga0 marked this conversation as resolved.
...SUPPORTED_LANGUAGES.map((l) => l.code),
'auto',
];
if (
typeof language !== 'string' ||
!allowedLanguages.includes(language)
) {
throw RequestError.invalidParams(
undefined,
`Invalid language; must be one of: ${allowedLanguages.join(', ')}`,
);
}

this.sessionOrThrow(sessionId);

try {
await setLanguageAsync(language);
Comment thread
chiga0 marked this conversation as resolved.
} catch (err) {
debugLogger.warn('setLanguageAsync failed:', err);
throw new RequestError(
-32603,
`Failed to switch UI language: ${err instanceof Error ? err.message : String(err)}`,
);
}

const resolvedLanguage = getCurrentLanguage();

Comment thread
chiga0 marked this conversation as resolved.
try {
this.settings.setValue(
SettingScope.User,
'general.language',
language,
);
} catch (err) {
debugLogger.warn('Failed to persist UI language setting:', err);
}

let outputLanguage: string | null = null;
let refreshed = false;

if (syncOutputLanguage) {
const resolved = resolveOutputLanguage(language);
const settingValue = isAutoLanguage(language)
? OUTPUT_LANGUAGE_AUTO
: resolved;

try {
updateOutputLanguageFile(settingValue);
Comment thread
chiga0 marked this conversation as resolved.
} catch (err) {
debugLogger.warn('Failed to write output-language.md:', err);
Comment thread
chiga0 marked this conversation as resolved.
}

try {
this.settings.setValue(
SettingScope.User,
'general.outputLanguage',
settingValue,
);
} catch (err) {
debugLogger.warn('Failed to persist output language setting:', err);
}

const allSessions = [...this.sessions.values()];
const results = await Promise.allSettled(
allSessions.map(async (s) => {
const cfg = s.getConfig();
await cfg.refreshHierarchicalMemory();
await cfg.getGeminiClient()?.refreshSystemInstruction();
}),
Comment thread
chiga0 marked this conversation as resolved.
Outdated
Comment thread
chiga0 marked this conversation as resolved.
Outdated
Comment thread
chiga0 marked this conversation as resolved.
Outdated
);
const failedCount = results.filter(
(r) => r.status === 'rejected',
).length;
if (failedCount > 0) {
debugLogger.warn(
`Language refresh failed for ${failedCount}/${results.length} session(s)`,
);
}
Comment thread
chiga0 marked this conversation as resolved.
refreshed = failedCount < results.length;
Comment thread
chiga0 marked this conversation as resolved.
Outdated
outputLanguage = resolved;
}

return { language: resolvedLanguage, outputLanguage, refreshed };
}
case SERVE_CONTROL_EXT_METHODS.sessionRecap: {
// Generate a one-sentence "where did I leave off" summary.
// Best-effort: returns `null` on short history or model failure.
Expand Down
Loading