diff --git a/packages/cli/test/render.test.ts b/packages/cli/test/render.test.ts
index c78c8c7..2824169 100644
--- a/packages/cli/test/render.test.ts
+++ b/packages/cli/test/render.test.ts
@@ -147,12 +147,14 @@ describe("StreamRenderer", () => {
partialToolCall({
eventType: "delta",
name: "",
- arguments: '"description":"列出当前目录的文件"}',
+ arguments: '"description":"List files in the current directory"}',
toolCallId: "c9",
}),
);
r.handle(partialToolCall({ eventType: "stop", name: "", toolCallId: "c9" }));
- expect(stripAnsi(text())).toBe("[tool-c9] exec_command <- 列出当前目录的文件 ($ ls -la)\n");
+ expect(stripAnsi(text())).toBe(
+ "[tool-c9] exec_command <- List files in the current directory ($ ls -la)\n",
+ );
});
it("streams the command live when the schema has no description argument", () => {
diff --git a/packages/core/test/describe-image.test.ts b/packages/core/test/describe-image.test.ts
index 01fdcc2..f6d4028 100644
--- a/packages/core/test/describe-image.test.ts
+++ b/packages/core/test/describe-image.test.ts
@@ -129,10 +129,10 @@ describe("describe_image (the text-only-model variant of read_image)", () => {
it("image + custom prompt is sent to the vision model in a single shot, its text comes back, the result carries no images", async () => {
await writeFile(path.join(tmp, "a.png"), PNG_1X1);
- const { llm, calls } = fakeLLM("图里是一只企鹅。");
+ const { llm, calls } = fakeLLM("The image shows a penguin.");
const describer: VisionDescriberService = { modelId: "vis-1", createLLM: () => llm };
const { messages, result, text } = await run(
- { source: "a.png", prompt: "图里是什么动物?" },
+ { source: "a.png", prompt: "What animal is in the image?" },
tmp,
describer,
);
@@ -143,17 +143,17 @@ describe("describe_image (the text-only-model variant of read_image)", () => {
(m) => m.payload as { type: string; text?: string; image_url?: string },
);
expect(payloads[0]!.type).toBe("text");
- expect(payloads[0]!.text).toBe("图里是什么动物?");
+ expect(payloads[0]!.text).toBe("What animal is in the image?");
expect(payloads[1]!.type).toBe("image_url");
expect(payloads[1]!.image_url).toBe(`data:image/png;base64,${PNG_1X1.toString("base64")}`);
expect(text).toContain("described by vis-1");
- expect(text).toContain("图里是一只企鹅。");
+ expect(text).toContain("The image shows a penguin.");
// Streaming forward: the header line and description deltas are emitted as separate chunks (not buffered as a whole), and the complete text is not forwarded again.
const outputs = messages.map((m) => (m.payload as { output?: string }).output ?? "");
expect(outputs.length).toBeGreaterThanOrEqual(3); // header + >=2 description delta chunks
expect(outputs[0]).toContain("described by vis-1");
- expect(outputs.slice(1).join("")).toBe("图里是一只企鹅。");
+ expect(outputs.slice(1).join("")).toBe("The image shows a penguin.");
// Text-based description: the result carries no images (images never enter session history).
expect(result?.images).toBeUndefined();
expect(result?.stopReason).toBeUndefined(); // defaults to completed
@@ -200,7 +200,7 @@ describe("describe_image (the text-only-model variant of read_image)", () => {
it("Environment assembles the describe_image entry with the delegated-description implementation; definition matches the config", async () => {
await writeFile(path.join(tmp, "a.png"), PNG_1X1);
- const { llm } = fakeLLM("代读结果");
+ const { llm } = fakeLLM("delegated description result");
const env = new Environment({
workspaceDir: tmp,
toolConfig: {
@@ -235,7 +235,7 @@ describe("describe_image (the text-only-model variant of read_image)", () => {
};
expect(complete.type).toBe("tool_call_output");
expect(complete.stop_reason).toBe("completed");
- expect(complete.output).toContain("代读结果");
+ expect(complete.output).toContain("delegated description result");
expect(complete.images).toBeUndefined();
});
});
diff --git a/packages/core/test/markers.test.ts b/packages/core/test/markers.test.ts
index 6ea9506..1487ce2 100644
--- a/packages/core/test/markers.test.ts
+++ b/packages/core/test/markers.test.ts
@@ -79,19 +79,19 @@ describe("stripConversationMarkers (whole-message title cleaning)", () => {
// The skill-invocation block that wraps a first user message must not reach the title.
expect(
stripConversationMarkers(
- "[use_skills]\nskills: penguin-sdk, web-design\n[/use_skills]\n做一个 RAG 应用",
+ "[use_skills]\nskills: penguin-sdk, web-design\n[/use_skills]\nBuild a RAG app",
),
- ).toBe("做一个 RAG 应用");
+ ).toBe("Build a RAG app");
// Handoff and scheduled-task markers are stripped too; ordinary bracketed text stays.
- expect(stripConversationMarkers("[handoff_from]data_analyst[/handoff_from]继续分析")).toBe(
- "继续分析",
- );
+ expect(
+ stripConversationMarkers("[handoff_from]data_analyst[/handoff_from]continue the analysis"),
+ ).toBe("continue the analysis");
// The /model switch origin block (the new session's first message) must not leak into the title either.
expect(
stripConversationMarkers(
- "[model_switch_from]\nsession: session-01\ntrace: /t/x_001.jsonl\n[/model_switch_from]\n继续这个任务",
+ "[model_switch_from]\nsession: session-01\ntrace: /t/x_001.jsonl\n[/model_switch_from]\ncontinue this task",
),
- ).toBe("继续这个任务");
+ ).toBe("continue this task");
expect(stripConversationMarkers("render a
element")).toBe("render a
element");
expect(stripConversationMarkers("check the [config] section")).toBe(
"check the [config] section",
@@ -100,14 +100,18 @@ describe("stripConversationMarkers (whole-message title cleaning)", () => {
it("the old angle-bracket marker form is still stripped (material from old Traces)", () => {
expect(
- stripConversationMarkers("\nskills: web-design\n\n做一个落地页"),
- ).toBe("做一个落地页");
- expect(stripConversationMarkers("data_analyst继续分析")).toBe(
- "继续分析",
- );
+ stripConversationMarkers(
+ "\nskills: web-design\n\nBuild a landing page",
+ ),
+ ).toBe("Build a landing page");
expect(
- stripConversationMarkers("session: s1继续这个任务"),
- ).toBe("继续这个任务");
+ stripConversationMarkers("data_analystcontinue the analysis"),
+ ).toBe("continue the analysis");
+ expect(
+ stripConversationMarkers(
+ "session: s1continue this task",
+ ),
+ ).toBe("continue this task");
});
});
diff --git a/packages/core/test/session-title.test.ts b/packages/core/test/session-title.test.ts
index 6d52f19..f4e3d66 100644
--- a/packages/core/test/session-title.test.ts
+++ b/packages/core/test/session-title.test.ts
@@ -64,18 +64,18 @@ describe("session-title", () => {
const result = await generateTitleWithLLM(
fakeLLM(
[
- thinkingMessage("想一下"), // thinking does not count
- assistantText("「Tailwind 主题配置」。"),
+ thinkingMessage("thinking"), // thinking does not count
+ assistantText("「Tailwind theme setup」。"),
tokenUsage(emptyTokenCounts(), { cache_read: 1, cache_write: 2, output: 3, total: 6 }),
],
{ status: "completed" },
seen,
),
- { userText: "解释 @theme", assistantText: "好的……" },
+ { userText: "explain @theme", assistantText: "sure thing." },
);
- expect(result.title).toBe("Tailwind 主题配置");
+ expect(result.title).toBe("Tailwind theme setup");
expect(result.usage).toEqual({ cache_read: 1, cache_write: 2, output: 3, total: 6 });
- expect(seen[0]).toBe(buildTitlePrompt("解释 @theme", "好的……"));
+ expect(seen[0]).toBe(buildTitlePrompt("explain @theme", "sure thing."));
expect(seen[0]).toContain("SAME language");
});
@@ -105,23 +105,23 @@ describe("session-title", () => {
it("still generates with empty assistant material (tool-only turn): uses only the user request, prompt omits the assistant section", async () => {
const seen: string[] = [];
const result = await generateTitleWithLLM(
- fakeLLM([assistantText("配置 Tailwind 主题")], { status: "completed" }, seen),
- { userText: "帮我配置 @theme", assistantText: "" },
+ fakeLLM([assistantText("Configure the Tailwind theme")], { status: "completed" }, seen),
+ { userText: "help me configure @theme", assistantText: "" },
);
- expect(result.title).toBe("配置 Tailwind 主题");
- expect(seen[0]).toBe(buildTitlePrompt("帮我配置 @theme", ""));
+ expect(result.title).toBe("Configure the Tailwind theme");
+ expect(seen[0]).toBe(buildTitlePrompt("help me configure @theme", ""));
expect(seen[0]).not.toContain("[Assistant]");
});
it("sanitizeTitle: strips quotes/punctuation to a fixed point, collapses whitespace, truncates overlong input, returns null for empty", () => {
- expect(sanitizeTitle("“ 构建配置 说明 。”")).toBe("构建配置 说明");
- expect(sanitizeTitle("『标题』!")).toBe("标题");
+ expect(sanitizeTitle("“ Build config notes 。”")).toBe("Build config notes");
+ expect(sanitizeTitle("『Title』!")).toBe("Title");
expect(sanitizeTitle(" \n ")).toBeNull();
expect(sanitizeTitle("x".repeat(50))).toHaveLength(30);
// A leaked [use_skills] block is stripped from the model output.
- expect(sanitizeTitle("[use_skills]\nskills: web-design\n[/use_skills]\n构建落地页")).toBe(
- "构建落地页",
- );
+ expect(
+ sanitizeTitle("[use_skills]\nskills: web-design\n[/use_skills]\nBuild a landing page"),
+ ).toBe("Build a landing page");
});
it("Session.generateTitle: sends via createBareLLM; returns null when no factory is provided", async () => {
diff --git a/packages/landing/src/lib/blog.ts b/packages/landing/src/lib/blog.ts
index 34a7e09..d3919d7 100644
--- a/packages/landing/src/lib/blog.ts
+++ b/packages/landing/src/lib/blog.ts
@@ -36,7 +36,7 @@ export function parseAuthors(raw: string | undefined): string[] {
return authors.length > 0 ? authors : [DEFAULT_AUTHOR];
}
-/** Join authors for display: 顿号 in Chinese, comma otherwise. */
+/** Join authors for display: an ideographic comma in Chinese, an ASCII comma otherwise. */
export function formatAuthors(authors: string[], locale: Locale): string {
return authors.join(locale === "zh" ? "、" : ", ");
}
@@ -82,7 +82,8 @@ export function comparePosts(
}
/**
- * Format a YYYY-MM-DD post date for display ("July 20, 2026" / "2026年7月20日").
+ * Format a YYYY-MM-DD post date for display (e.g. "July 20, 2026" in English, the
+ * year-month-day form in Chinese).
* Parsed as UTC and formatted in UTC so the calendar day never shifts with the
* viewer's timezone; unexpected input falls back to the raw string.
*/
diff --git a/packages/landing/src/sections/hero.tsx b/packages/landing/src/sections/hero.tsx
index cf2b911..8421319 100644
--- a/packages/landing/src/sections/hero.tsx
+++ b/packages/landing/src/sections/hero.tsx
@@ -1,7 +1,7 @@
/**
* Hero: enlarged logo + product name, the slogan inside the pill badge, then the
* one-line headline whose rotating word crossfades through a gaussian blur
- * (Desktop <-> Server / 桌面 <-> 服务器), three keywords, the install one-liner
+ * (Desktop <-> Server, localized per dictionary), three keywords, the install one-liner
* and stats. The rotating word is a stacked inline-grid so line width never jumps.
*/
import { Fragment, useEffect, useState } from "react";
diff --git a/packages/landing/test/blog.test.ts b/packages/landing/test/blog.test.ts
index 403d6e1..bc7f8e2 100644
--- a/packages/landing/test/blog.test.ts
+++ b/packages/landing/test/blog.test.ts
@@ -36,7 +36,7 @@ describe("formatPostDate", () => {
expect(formatPostDate("2026-07-20", "en")).toBe("July 20, 2026");
});
- it("formats zh dates as 年月日", () => {
+ it("formats zh dates as year-month-day", () => {
expect(formatPostDate("2026-06-18", "zh")).toBe("2026年6月18日");
expect(formatPostDate("2026-07-20", "zh")).toBe("2026年7月20日");
});
@@ -68,9 +68,9 @@ describe("parseAuthors / formatAuthors", () => {
expect(parseAuthors(" ")).toEqual([DEFAULT_AUTHOR]);
});
- it("joins with a comma in English and 顿号 in Chinese", () => {
+ it("joins with a comma in English and an ideographic comma in Chinese", () => {
expect(formatAuthors(["A", "B"], "en")).toBe("A, B");
- expect(formatAuthors(["甲", "乙"], "zh")).toBe("甲、乙");
+ expect(formatAuthors(["A", "B"], "zh")).toBe("A、B");
expect(formatAuthors(["A"], "en")).toBe("A");
});
});
diff --git a/packages/server/test/models.test.ts b/packages/server/test/models.test.ts
index 2b1d1c5..73e2031 100644
--- a/packages/server/test/models.test.ts
+++ b/packages/server/test/models.test.ts
@@ -45,7 +45,7 @@ describe("models preset & catalog enrichment", () => {
const { cookie } = await provisionUser(t.app, "alice");
api = apiClient(t.app, cookie);
const created = (await (
- await api.post("/api/projects", { projectId: "alice-preset", name: "预置项目" })
+ await api.post("/api/projects", { projectId: "alice-preset", name: "Preset project" })
).json()) as ProjectCreateResponse;
projectId = created.project.projectId;
});
@@ -53,7 +53,7 @@ describe("models preset & catalog enrichment", () => {
await t.cleanup();
});
- it("credential 内联单文件:PUT 的 apiKey 落 .project_config.toml(0600),GET 只回掩码", async () => {
+ it("credentials are inlined in one file: the apiKey from PUT lands in .project_config.toml (0600), GET returns only a mask", async () => {
const put = await api.put(url(), {
defaultModel: { provider: "custom", modelId: "m-inline" },
models: [
@@ -93,7 +93,7 @@ describe("models preset & catalog enrichment", () => {
await expect(readFile(path.join(projectDir, "project_config.toml"), "utf8")).rejects.toThrow();
});
- it("新建 Project 即预置全部内置模型(provider 与 model_id 分列 + 目录信息)", async () => {
+ it("a new Project is preset with every built-in model (provider and model_id as separate fields + catalog info)", async () => {
const res = await api.get(url());
expect(res.status).toBe(200);
const body = (await res.json()) as ModelsResponse;
@@ -125,7 +125,7 @@ describe("models preset & catalog enrichment", () => {
expect(mimo.credential?.apiKeyMasked).toBeUndefined();
});
- it("PUT 自定义模型:持久化 vision 标注;openai 协议给 OPENAI_API_KEY 兜底;provider 必填", async () => {
+ it("PUT a custom model: the vision flag persists; the openai protocol falls back to OPENAI_API_KEY; provider is required", async () => {
const put = await api.put(url(), {
defaultModel: { provider: "custom", modelId: "my-model" },
models: [
@@ -167,7 +167,7 @@ describe("models preset & catalog enrichment", () => {
expect(noProvider.status).toBe(400);
});
- it("PUT maxTokens:落盘为 max_tokens 并经 GET 回读;整表省略即清除;0/负数/非数字 400", async () => {
+ it("PUT maxTokens: persisted as max_tokens and read back through GET; omitting it table-wide clears it; 0 / negative / non-numeric 400", async () => {
const put = await api.put(url(), {
models: [
{ provider: "custom", modelId: "local-qwen", clientType: "openai", maxTokens: 8000 },
@@ -204,7 +204,7 @@ describe("models preset & catalog enrichment", () => {
}
});
- it("同名 model_id 可在不同 provider 下并存(成对键,互不覆盖)", async () => {
+ it("the same model_id can coexist under different providers (paired keys, neither overwrites the other)", async () => {
const put = await api.put(url(), {
models: [
{ provider: "moonshot", modelId: "kimi-k2.6", apiKey: "sk-official-aaaa1111" },
@@ -231,7 +231,7 @@ describe("models preset & catalog enrichment", () => {
});
});
-describe("default_project 预置", () => {
+describe("default_project presets", () => {
let t: TestApp;
let prevKey: string | undefined;
@@ -247,7 +247,7 @@ describe("default_project 预置", () => {
await t.cleanup();
});
- it("种子 admin 纳管 default_project 时补齐预置模型与默认模型(此前无 .project_config.toml)", async () => {
+ it("when the seeded admin adopts default_project, the preset models and the default model are filled in (no prior .project_config.toml)", async () => {
// The default_project shared by admin seeding and the CLI (the dir already exists, so writeInitialConfig is skipped).
t = await createTestApp();
const { cookie } = await loginAdmin(t.app);
@@ -269,7 +269,7 @@ describe("default_project 预置", () => {
expect(session.modelId).toBe("deepseek-v4-pro");
});
- it("已配置过模型的 default_project 原样保留(不覆盖 CLI 既有配置)", async () => {
+ it("a default_project that already has models configured is left untouched (existing CLI config is not overwritten)", async () => {
// First have the "CLI" write a config with a single custom model, then admin seeding adopts it.
t = await createTestApp({
beforeSeed: async (root) => {
@@ -290,7 +290,7 @@ describe("default_project 预置", () => {
});
});
-describe("模型引用改键与连通性测试", () => {
+describe("model-reference rekeying and the connectivity test", () => {
let t: TestApp;
let api: ReturnType;
let projectId: string;
@@ -302,7 +302,7 @@ describe("模型引用改键与连通性测试", () => {
const { cookie } = await provisionUser(t.app, "carol");
api = apiClient(t.app, cookie);
const created = (await (
- await api.post("/api/projects", { projectId: "carol-rename", name: "改名项目" })
+ await api.post("/api/projects", { projectId: "carol-rename", name: "Rename project" })
).json()) as ProjectCreateResponse;
projectId = created.project.projectId;
});
@@ -310,7 +310,7 @@ describe("模型引用改键与连通性测试", () => {
await t.cleanup();
});
- it("renamedFrom 迁移 credential 与配置,默认/视觉模型指针跟随改键", async () => {
+ it("renamedFrom migrates the credential and the config; the default / vision model pointers follow the rekey", async () => {
await api.put(url(), {
defaultModel: { provider: "custom", modelId: "old-id" },
visionModel: { provider: "custom", modelId: "old-id" },
@@ -347,7 +347,7 @@ describe("模型引用改键与连通性测试", () => {
expect(again.defaultModel).toEqual({ provider: "custom", modelId: "new-id" });
});
- it("分组变更也是改键:credential 随迁;envKey 按 client 解析、不随分组(PRN-021)", async () => {
+ it("a group change is a rekey too: the credential migrates with it; envKey resolves by client, not by group (PRN-021)", async () => {
// Move the preset DeepSeek model to another group (changing provider is a key change; the paired renamedFrom migrates it).
const put = await api.put(url(), {
models: [
@@ -384,28 +384,28 @@ describe("模型引用改键与连通性测试", () => {
expect(moved.envKey).toBe("DEEPSEEK_API_KEY");
});
- it("展示名可编辑:与内置目录一致时不落盘;provider 恒作为条目字段落盘", async () => {
+ it("the display name is editable: not persisted when it matches the built-in catalog; provider is always persisted as an entry field", async () => {
// Preset model: saved as-is → the display name falls back to the built-in catalog, and display_name isn't written to the config file.
await api.put(url(), {
models: [
{ provider: "openai", modelId: "gpt-5.5", displayName: "GPT-5.5" },
- { provider: "openai", modelId: "gpt-5.4", displayName: "我的 GPT" },
+ { provider: "openai", modelId: "gpt-5.4", displayName: "My GPT" },
],
});
const body = (await (await api.get(url())).json()) as ModelsResponse;
expect(pick(body, "openai", "gpt-5.5").displayName).toBe("GPT-5.5");
// Edited one: the display name takes effect per the user's setting.
- expect(pick(body, "openai", "gpt-5.4").displayName).toBe("我的 GPT");
+ expect(pick(body, "openai", "gpt-5.4").displayName).toBe("My GPT");
// Clean on disk: unchanged preset models don't write display_name; provider is stored as a separate column, no concatenated string.
const toml = await readFile(path.join(t.root, projectId, ".project_config.toml"), "utf8");
expect(toml).not.toContain('display_name = "GPT-5.5"');
- expect(toml).toContain('display_name = "我的 GPT"');
+ expect(toml).toContain('display_name = "My GPT"');
expect(toml).toContain('provider = "openai"');
expect(toml).not.toContain("openai/gpt-5.5");
});
- it("renamedFrom 非法值 400;不带 renamedFrom 改键等于删旧建新(credential 不迁移)", async () => {
+ it("an invalid renamedFrom is 400; rekeying without renamedFrom equals delete-old-then-create-new (the credential does not migrate)", async () => {
await api.put(url(), {
models: [{ provider: "custom", modelId: "m-a", apiKey: "sk-secret-abcd1234" }],
});
@@ -426,7 +426,7 @@ describe("模型引用改键与连通性测试", () => {
expect(body.models[0]!.credential).toBeUndefined();
});
- it("连通性测试发的是条目的上游 model_id(引用成对随请求体)", async () => {
+ it("the connectivity test sends the entry's upstream model_id (the reference pair travels with the request body)", async () => {
// Local openai-compatible endpoint: records the model field from the request body, then always rejects with 401 (never hits the network).
const seenModels: string[] = [];
const server = createServer((req, res) => {
@@ -472,7 +472,7 @@ describe("模型引用改键与连通性测试", () => {
}
});
- it("连通性测试请求体不含 tools 与 tool_choice(空工具列表整体省略,vLLM 等严格端点不再 400)", async () => {
+ it("the connectivity-test request body carries no tools or tool_choice (an empty tool list is omitted entirely, so strict endpoints such as vLLM no longer 400)", async () => {
// The probe runs with an empty tool list. The wire body must omit `tools` entirely —
// `tools: []` is rejected by strict OpenAI-compatible servers (vLLM: "tools must not be an
// empty array") — and must never carry `tool_choice`.
@@ -512,7 +512,7 @@ describe("模型引用改键与连通性测试", () => {
}
});
- it("连通性测试:已保存的模型与**尚未保存**的自定义模型都可测(LLM 层不抛异常,一律收敛)", async () => {
+ it("connectivity test: both a saved model and a **not-yet-saved** custom model can be tested (the LLM layer throws nothing, every outcome converges)", async () => {
await api.put(url(), {
models: [{ provider: "openai", modelId: "gpt-5.5", apiKey: "sk-invalid-key-for-test" }],
});
@@ -536,7 +536,7 @@ describe("模型引用改键与连通性测试", () => {
expect(typeof unsavedBody.message).toBe("string");
}, 40_000);
- it("连通性测试:模型完全没有 credential 时收敛为 ok:false,而非 500", async () => {
+ it("connectivity test: a model with no credential at all converges to ok:false instead of 500", async () => {
// A model using the OpenAI protocol: the provider SDK throws at **client construction** because
// the key is missing — if that construction were outside the try it would bubble up as a 500. Clear the env-var key so there's nowhere to get one (no real network request).
const prev = process.env.OPENAI_API_KEY;
@@ -555,7 +555,7 @@ describe("模型引用改键与连通性测试", () => {
}
});
- it("连通性测试:clearApiKey 时不回落已存 key(照当前草稿测)", async () => {
+ it("connectivity test: clearApiKey does not fall back to the stored key (the current draft is what gets tested)", async () => {
// A key is already saved, but the test request carries clearApiKey — the server must **not** use
// the saved key. Clear the env var so "don't use the saved key" == no credential at all, so construction synchronously throws missing-credential (no network request, deterministic).
const prev = process.env.OPENAI_API_KEY;
diff --git a/packages/server/test/title-generator.test.ts b/packages/server/test/title-generator.test.ts
index 97ddece..fb39a54 100644
--- a/packages/server/test/title-generator.test.ts
+++ b/packages/server/test/title-generator.test.ts
@@ -100,21 +100,21 @@ describe("title-generator", () => {
CTX,
fakeSession(
{
- title: "Tailwind 主题配置",
+ title: "Tailwind theme setup",
usage: { cache_read: 1, cache_write: 2, output: 3, total: 6 },
},
calls,
),
- { fallbackText: "解释一下 @theme" },
+ { fallbackText: "explain @theme" },
);
await waitFor(() => sessions.findById(ROW.sessionId)?.title !== null);
- expect(sessions.findById(ROW.sessionId)?.title).toBe("Tailwind 主题配置");
+ expect(sessions.findById(ROW.sessionId)?.title).toBe("Tailwind theme setup");
// No material override passed: generateTitle is called with no argument, and the core Session gathers its own material.
expect(calls.args[0]).toBeUndefined();
expect(
serverEvents(events).some(
- (e) => e.type === "session_title" && e.title === "Tailwind 主题配置",
+ (e) => e.type === "session_title" && e.title === "Tailwind theme setup",
),
).toBe(true);
// usage is converted into token_usage and handed to the recorder (metered normally, same as a real call).
@@ -125,14 +125,14 @@ describe("title-generator", () => {
it("material override (sub-session scenario) is passed to generateTitle verbatim", async () => {
const calls = { count: 0, args: [] as unknown[] };
const gen = makeGenerator();
- const material = { userText: "子会话 prompt", assistantText: "子会话回答" };
- gen.maybeGenerate(CTX, fakeSession({ title: "子标题", usage: null }, calls), {
- fallbackText: "子会话 prompt",
+ const material = { userText: "sub-session prompt", assistantText: "sub-session reply" };
+ gen.maybeGenerate(CTX, fakeSession({ title: "Sub title", usage: null }, calls), {
+ fallbackText: "sub-session prompt",
material,
});
await waitFor(() => sessions.findById(ROW.sessionId)?.title !== null);
expect(calls.args[0]).toEqual({ material });
- expect(sessions.findById(ROW.sessionId)?.title).toBe("子标题");
+ expect(sessions.findById(ROW.sessionId)?.title).toBe("Sub title");
});
it("an existing title is never regenerated (no one-shot request issued)", async () => {
@@ -155,11 +155,11 @@ describe("title-generator", () => {
{ title: null, usage: { cache_read: 0, cache_write: 0, output: 1, total: 1 } },
calls,
),
- { fallbackText: "配置 Tailwind 主题\n第二行" },
+ { fallbackText: "Configure the Tailwind theme\nsecond line" },
);
// Fallback = the material's first non-empty line, sanitized and truncated, guaranteeing a title is always produced.
await waitFor(() => sessions.findById(ROW.sessionId)?.title !== null);
- expect(sessions.findById(ROW.sessionId)?.title).toBe("配置 Tailwind 主题");
+ expect(sessions.findById(ROW.sessionId)?.title).toBe("Configure the Tailwind theme");
// The one-off request's usage is still recorded normally.
await waitFor(() => recorded.length > 0);
});
@@ -168,21 +168,21 @@ describe("title-generator", () => {
const calls = { count: 0, args: [] as unknown[] };
const gen = makeGenerator();
gen.maybeGenerate(CTX, fakeSession({ title: null, usage: null }, calls), {
- fallbackText: "[use_skills]\nskills: web-design\n[/use_skills]\n做一个落地页",
+ fallbackText: "[use_skills]\nskills: web-design\n[/use_skills]\nBuild a landing page",
});
await waitFor(() => sessions.findById(ROW.sessionId)?.title !== null);
// Not "[use_skills]" — the marker block is removed before the first line is taken.
- expect(sessions.findById(ROW.sessionId)?.title).toBe("做一个落地页");
+ expect(sessions.findById(ROW.sessionId)?.title).toBe("Build a landing page");
});
it("fallback also strips the legacy angle-bracket block (material from old Traces)", async () => {
const calls = { count: 0, args: [] as unknown[] };
const gen = makeGenerator();
gen.maybeGenerate(CTX, fakeSession({ title: null, usage: null }, calls), {
- fallbackText: "\nskills: web-design\n\n做一个落地页",
+ fallbackText: "\nskills: web-design\n\nBuild a landing page",
});
await waitFor(() => sessions.findById(ROW.sessionId)?.title !== null);
- expect(sessions.findById(ROW.sessionId)?.title).toBe("做一个落地页");
+ expect(sessions.findById(ROW.sessionId)?.title).toBe("Build a landing page");
});
it("LLM returns null and the fallback material is blank → the title stays NULL (retryable next time)", async () => {
diff --git a/packages/web/src/components/layout/sidebar.tsx b/packages/web/src/components/layout/sidebar.tsx
index be5a754..7a3c6fd 100644
--- a/packages/web/src/components/layout/sidebar.tsx
+++ b/packages/web/src/components/layout/sidebar.tsx
@@ -1236,7 +1236,7 @@ export function Sidebar({
* pinned indicator. The header row carries the `group/header` scope so the reveal only
* reacts to its own row, not to the session rows' plain `group` scope.
* The accessible name stays STATIC and aria-pressed alone carries the state (the toggle
- * pattern the grouping-mode buttons use) — a name that swaps 置顶/取消置顶 alongside
+ * pattern the grouping-mode buttons use) — a name that swaps Pin/Unpin alongside
* aria-pressed reads as "Unpin group, pressed", saying the state twice in conflicting
* ways. The title tooltip may still swap: it is presentation for pointer users and does
* not feed the accessible name while aria-label is present.
diff --git a/packages/web/src/features/agents/agent-settings-page.tsx b/packages/web/src/features/agents/agent-settings-page.tsx
index 0503313..041faa2 100644
--- a/packages/web/src/features/agents/agent-settings-page.tsx
+++ b/packages/web/src/features/agents/agent-settings-page.tsx
@@ -44,7 +44,7 @@ type TabKey = "overview" | "prompt" | "runtime" | "tools" | "vault" | "schedules
* The "" (not-overridden / inherit) row is filtered out per review — the menus offer only
* concrete values in dictionary order, the user picks explicitly. An unset stored value
* simply matches no row, so the OptionMenu trigger falls back to its placeholder
- * ((缺省)/(default), the same convention as the tools-table permission menu); nothing is
+ * ("(default)", the same convention as the tools-table permission menu); nothing is
* ever written silently, and the reset link next to each menu rewinds a local pick back to "".
*/
export function optionRows(
diff --git a/packages/web/src/features/chat/approval-buttons.tsx b/packages/web/src/features/chat/approval-buttons.tsx
index 5269a8a..3a93531 100644
--- a/packages/web/src/features/chat/approval-buttons.tsx
+++ b/packages/web/src/features/chat/approval-buttons.tsx
@@ -24,7 +24,7 @@ export function ApprovalButtons({
};
// Text at every breakpoint (per review): action buttons the user presses must read as words —
- // "允许/Allow" and "拒绝/Deny" — never as bare glyphs; iconic shorthand is reserved for
+ // "Allow" and "Deny" — never as bare glyphs; iconic shorthand is reserved for
// passive indicators. The buttons live on their own row under the (wrapping) argument
// preview, so the words cost no width the pending card doesn't already have.
return (
diff --git a/packages/web/src/features/chat/draft-view.tsx b/packages/web/src/features/chat/draft-view.tsx
index 62be940..6790618 100644
--- a/packages/web/src/features/chat/draft-view.tsx
+++ b/packages/web/src/features/chat/draft-view.tsx
@@ -683,8 +683,8 @@ const versionBadgeClass =
"ml-1.5 inline-block rounded-full bg-[var(--accent-bg)] px-1.5 align-super text-[10px] font-medium leading-4 text-[var(--accent-fg)] transition-opacity duration-150 hover:opacity-80";
/**
- * Quiet version line under the brand subtitle: `vX.Y.Z · 最近更新日期 7 月 26 日` /
- * `… · Last updated Jul 26`. The product name is not repeated here — the brand wordmark
+ * Quiet version line under the brand subtitle: `vX.Y.Z · Last updated Jul 26`
+ * (localized per dictionary). The product name is not repeated here — the brand wordmark
* sits directly above, and the sidebar's version footer is bare `vX.Y.Z` too. The date is
* the running version's release
* date, stamped into core's BUILD_DATE at build time — displayed as-is, no network;
diff --git a/packages/web/src/features/chat/tool-call-card.tsx b/packages/web/src/features/chat/tool-call-card.tsx
index ea2cb4e..f35dce7 100644
--- a/packages/web/src/features/chat/tool-call-card.tsx
+++ b/packages/web/src/features/chat/tool-call-card.tsx
@@ -205,7 +205,7 @@ export function ToolCallCard({ item, ctx }: { item: ToolCallItem; ctx: StreamRen
: failed
? "failed"
: "done";
- // Decision wording ("Approved · manual" / "已拒绝 · 手动" …): carried ONLY by the left status
+ // Decision wording ("Approved · manual", "Denied · manual", …): carried ONLY by the left status
// icon's title/aria-label — per review the row shows no visible decision text at any
// breakpoint; the icon is the single source of truth for how the call was decided.
const decisionText = item.decision
diff --git a/packages/web/src/features/skills/skills-page.tsx b/packages/web/src/features/skills/skills-page.tsx
index 76c7e9b..786a79b 100644
--- a/packages/web/src/features/skills/skills-page.tsx
+++ b/packages/web/src/features/skills/skills-page.tsx
@@ -21,16 +21,16 @@
* Agent's v_old → v_new and warns the overwriting reinstall drops local
* edits), then reinstalls the current library copy on every outdated Agent
* (install-again-is-update semantics), with a single success toast; the
- * manage-installs Modal marks outdated rows with an accent "更新"/"Update"
- * button doing the same per Agent (through the same confirm);
+ * manage-installs Modal marks outdated rows with an accent "Update" button
+ * doing the same per Agent (through the same confirm);
* - Paper plane "quick invoke": enters /chat/new draft mode with default_agent,
* pre-selects the skill, and pre-fills the invocation text per UI language
- * (zh "使用 X 技能" / en "use the X skill", overwriting any existing draft body);
+ * ("use the X skill" in the active dictionary, overwriting any existing draft body);
* - Download "manage installs": a Modal listing every Agent in the current
- * Project — not-installed shows "安装"/"Install", installed shows
- * "已安装"/"Installed" (hover switches to "卸载"/"Uninstall", click to
- * uninstall); any member can operate it; optimistic update, a top-level
- * toast on success for install/uninstall, rollback plus a toast on failure.
+ * Project — not-installed shows "Install", installed shows "Installed"
+ * (hover switches to "Uninstall", click to uninstall); any member can
+ * operate it; optimistic update, a top-level toast on success for
+ * install/uninstall, rollback plus a toast on failure.
*/
import { useEffect, useState } from "react";
import { useNavigate } from "react-router";
@@ -550,11 +550,11 @@ function SkillCard({
/**
* One Agent row in the "manage installs" Modal: not-installed shows
- * "安装"/"Install"; installed shows "已安装"/"Installed", switching to
- * "卸载"/"Uninstall" on hover (same button, click to uninstall); an installed
- * copy older than the library additionally shows an accent "更新"/"Update"
- * button (reinstall = update). Install and uninstall go through optimistic
- * updates (toggleInstall), rolling back on failure.
+ * "Install"; installed shows "Installed", switching to "Uninstall" on hover
+ * (same button, click to uninstall); an installed copy older than the library
+ * additionally shows an accent "Update" button (reinstall = update). Install
+ * and uninstall go through optimistic updates (toggleInstall), rolling back on
+ * failure.
*/
function InstallRow({
agentId,
@@ -589,7 +589,7 @@ function InstallRow({
)}
{installed ? (
- // group: on hover the button's copy switches "已安装"/"Installed" → "卸载"/"Uninstall" (the same button carries the uninstall action).
+ // group: on hover the button's copy switches "Installed" → "Uninstall" (the same button carries the uninstall action).