Skip to content

Commit 7181265

Browse files
committed
feat(ai): sync reply language with the active UI locale
Include the effective Luna locale in Kael context snapshots and refresh it before the next run after a language change. Preserve uploaded context and command language across browser and Electron requests.
1 parent 622a172 commit 7181265

3 files changed

Lines changed: 84 additions & 4 deletions

File tree

‎ui/app.vue‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import type { DesktopUnlistenFn } from "~/shared/desktop/bridge";
33
import type { LangType, LanguagePreference } from "~/types";
44
5+
import { agentClient } from "#koko/composables/agent/agentClient";
56
import defaultFavicon from "~/assets/facio.ico";
67
import AppWatermark from "~/components/AppWatermark.vue";
78
import AclDialog from "~/components/Modal/aclDialog.vue";
@@ -31,6 +32,10 @@ const webWorkspaceFavicon = useState<string>(WORKSPACE_FAVICON_STATE_KEY, () =>
3132
3233
const { isMacOS, isWindows } = usePlatform();
3334
const { locale, setLocale, t } = useI18n();
35+
watch(locale, (value) => agentClient.setResponseLanguage(normalizeLanguageCode(value)), {
36+
immediate: true,
37+
flush: "sync"
38+
});
3439
const { userTheme, applyThemePreference, applySystemThemePreference } = useThemeAdapter();
3540
3641
const { applyPrimaryColor } = useColor();

‎ui/koko/composables/agent/agentClient.ts‎

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
AgentSessionCreateResponse,
1111
AgentToolResultRequest
1212
} from "./types";
13+
import type { LangType } from "~/types";
1314
import { desktopInvoke } from "~/shared/desktop/bridge";
1415
import { getWebApiHeaders, getWebApiMutationHeaders, isDesktopRuntime, withWebSitePrefix } from "~/utils/runtime";
1516
import { normalizeAgentEvent } from "./agentSse";
@@ -78,6 +79,7 @@ interface AgentBinding {
7879
surface: string;
7980
profile: string;
8081
contextVersion: number;
82+
context: Record<string, unknown>;
8183
heartbeat: ReturnType<typeof setInterval>;
8284
}
8385

@@ -161,9 +163,24 @@ export class AgentClient {
161163
private readonly instanceByResource = new Map<string, string>();
162164
private readonly usersByResource = new Map<string, number>();
163165
private readonly bindings = new Map<string, AgentBinding>();
166+
private responseLanguage?: LangType;
164167

165168
constructor(private readonly request: AgentRequest = defaultAgentRequest) {}
166169

170+
setResponseLanguage(language: LangType) {
171+
this.responseLanguage = language;
172+
}
173+
174+
private contextWithResponseLanguage(context: Record<string, unknown> = {}): Record<string, unknown> {
175+
// Keep the uploaded snapshot independent of reactive workspace state.
176+
return JSON.parse(
177+
JSON.stringify({
178+
...context,
179+
...(this.responseLanguage ? { response_language: this.responseLanguage } : {})
180+
})
181+
);
182+
}
183+
167184
retainResource(resourceSessionId: string) {
168185
this.usersByResource.set(resourceSessionId, (this.usersByResource.get(resourceSessionId) || 0) + 1);
169186
}
@@ -243,7 +260,8 @@ export class AgentClient {
243260
}
244261
});
245262
let contextVersion = 0;
246-
if (manifest.context) {
263+
const contextData = this.contextWithResponseLanguage(manifest.context);
264+
if (manifest.context || this.responseLanguage) {
247265
const context = await this.request<KaelContext>({
248266
method: "PUT",
249267
path: sessionPath(panel.id, "context"),
@@ -252,7 +270,7 @@ export class AgentClient {
252270
domain: manifest.profile,
253271
surface,
254272
sensitivity: "restricted",
255-
data: manifest.context
273+
data: contextData
256274
}
257275
});
258276
contextVersion = Math.max(0, Math.floor(Number(context.version) || 1));
@@ -285,6 +303,7 @@ export class AgentClient {
285303
surface,
286304
profile: manifest.profile,
287305
contextVersion,
306+
context: contextData,
288307
heartbeat
289308
});
290309
const registrationIds = Object.fromEntries(
@@ -314,6 +333,9 @@ export class AgentClient {
314333
message: AgentMessageRequest
315334
): Promise<AgentMessageResponse> {
316335
const binding = this.binding(sessionId, resourceSessionId);
336+
if (this.responseLanguage && binding.context.response_language !== this.responseLanguage) {
337+
await this.updateContext(sessionId, resourceSessionId, binding.context);
338+
}
317339
const created = await this.request<KaelMessage>({
318340
method: "POST",
319341
path: `${KAEL_API_ROOT}/conversations/${binding.conversationId}/messages`,
@@ -342,6 +364,7 @@ export class AgentClient {
342364

343365
async updateContext(sessionId: string, resourceSessionId: string, context: Record<string, unknown>) {
344366
const binding = this.binding(sessionId, resourceSessionId);
367+
const contextData = this.contextWithResponseLanguage(context);
345368
const response = await this.request<KaelContext>({
346369
method: "PUT",
347370
path: sessionPath(binding.panelId, "context"),
@@ -350,10 +373,11 @@ export class AgentClient {
350373
domain: binding.profile,
351374
surface: binding.surface,
352375
sensitivity: "restricted",
353-
data: context
376+
data: contextData
354377
}
355378
});
356379
binding.contextVersion = Math.max(binding.contextVersion + 1, Math.floor(Number(response.version) || 0));
380+
binding.context = contextData;
357381
}
358382

359383
async resolveApproval(

‎ui/koko/tests/agent/agentClient.test.ts‎

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,56 @@ it("forwards the executor command policy without marking every shell call read-o
133133
}
134134
});
135135

136+
it("includes the response language even without an executor context", async () => {
137+
const requests: AgentHttpRequest[] = [];
138+
const client = new AgentClient(kaelRequest(requests));
139+
client.setResponseLanguage("en");
140+
try {
141+
await client.createSession({ ...manifest, context: undefined }, "auto");
142+
expect(requests.find((request) => request.path.endsWith("/context"))?.body).toMatchObject({
143+
data: { response_language: "en" }
144+
});
145+
} finally {
146+
client.dispose();
147+
}
148+
});
149+
150+
it("refreshes response language before the next run without changing the command language or user text", async () => {
151+
const requests: AgentHttpRequest[] = [];
152+
const client = new AgentClient(kaelRequest(requests));
153+
client.setResponseLanguage("zh");
154+
const context = reactive({ language: "shell", selected_asset: { id: "asset-1" }, response_language: "en" });
155+
const message = {
156+
message_id: "message-1",
157+
idempotency_key: "send-1",
158+
role: "user" as const,
159+
parts: [{ type: "text" as const, text: "inspect" }]
160+
};
161+
try {
162+
await client.createSession(manifest, "auto");
163+
await client.updateContext("panel-1", "resource-1", context);
164+
expect(requests.filter((request) => request.path.endsWith("/context")).at(-1)?.body).toMatchObject({
165+
data: { language: "shell", selected_asset: { id: "asset-1" }, response_language: "zh" }
166+
});
167+
// Switching UI language must resend the last uploaded snapshot, not later reactive edits.
168+
context.selected_asset.id = "asset-2";
169+
requests.length = 0;
170+
client.setResponseLanguage("zh_hant");
171+
await client.sendMessage("panel-1", "resource-1", message);
172+
expect(requests.map((request) => request.path.split("/").at(-1))).toEqual(["context", "messages", "runs"]);
173+
expect(requests[0]?.body).toMatchObject({
174+
base_version: 2,
175+
data: { language: "shell", selected_asset: { id: "asset-1" }, response_language: "zh_hant" }
176+
});
177+
expect(requests[1]?.body).toMatchObject({ parts: message.parts });
178+
requests.length = 0;
179+
await client.sendMessage("panel-1", "resource-1", { ...message, idempotency_key: "send-2" });
180+
expect(requests.map((request) => request.path.split("/").at(-1))).toEqual(["messages", "runs"]);
181+
} finally {
182+
client.dispose();
183+
}
184+
});
185+
136186
it("maps messages, runs, approvals, and tool results to canonical Kael resources", async () => {
137187
const requests: AgentHttpRequest[] = [];
138188
const client = new AgentClient(kaelRequest(requests));
@@ -204,6 +254,7 @@ it.each([false, true])("sends reactive AI context, messages and results as JSON
204254
);
205255
const client = new AgentClient();
206256
try {
257+
client.setResponseLanguage("pt_br");
207258
await client.createSession({ ...manifest, profile: "workspace" }, "auto");
208259
const state = reactive({ default_terminal_target: { target_id: "target-1", asset_name: "host-a" } });
209260
// Spreading a reactive context unwraps only its root; the target remains a Vue Proxy.
@@ -225,7 +276,7 @@ it.each([false, true])("sends reactive AI context, messages and results as JSON
225276
});
226277
state.default_terminal_target.asset_name = "host-b";
227278
expect(requests.filter((request) => request.path.endsWith("/context")).at(-1)?.body).toMatchObject({
228-
data: { default_terminal_target: { target_id: "target-1", asset_name: "host-a" } }
279+
data: { response_language: "pt_br", default_terminal_target: { target_id: "target-1", asset_name: "host-a" } }
229280
});
230281
expect(requests.find((request) => request.path.endsWith("/messages"))?.body).toMatchObject({
231282
parts: [{ type: "text", text: "Inspect the current terminal" }]

0 commit comments

Comments
 (0)