From 8870cfc7dc7f1e7a8221dc2b16ebfd4aed7779d0 Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Sun, 18 Oct 2026 08:10:17 +0200 Subject: [PATCH] fix(#646): translate the Pin tab names row and list it in the Settings reference The new App Settings row had no zh-CN entry, so it read in English in a Chinese UI, and docs/wiki/Settings-Reference.md did not list it. Adds the label and description to i18n.js, a row in the Agents & CLIs table, and a JSDOM test that runs the real translator over the real index.html. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/wiki/Settings-Reference.md | 1 + src/web/public/i18n.js | 3 ++ test/pin-cli-session-name-i18n.test.ts | 66 ++++++++++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 test/pin-cli-session-name-i18n.test.ts diff --git a/docs/wiki/Settings-Reference.md b/docs/wiki/Settings-Reference.md index 8f764750..2e7238ce 100644 --- a/docs/wiki/Settings-Reference.md +++ b/docs/wiki/Settings-Reference.md @@ -150,6 +150,7 @@ instead of its native cloud backend. See [Custom Model Endpoints](Custom-Model-E | Allowed Tools | The list used by the explicit mode. | | Ralph / Todo Tracker | Enables the Ralph loop surfaces. | | Agent Teams | Experimental teams. Also needs the CLI's own environment flag. | +| Pin tab names on the CLI | Launches every Claude session with `--name` set to its tab name, so Claude Code's prompt label and `/resume` list match your tabs. Claude then stops generating its own titles for those sessions, and a rename in Codeman holds only until the next turn. Synced, off by default; applies from the next session start or respawn. | | Codeman Agent Skill | Under **All agent CLIs**. Injects the agent skill into each new session's case, where that session's CLI looks for skills (every CLI except Pi and a plain shell). Off by default. See [The Codeman Skill](Codeman-Skill). | | Remote auto-reconnect | Reattaches dropped remote SSH sessions. On by default. | | Nice priority / value | Runs agent processes at a lower CPU priority. | diff --git a/src/web/public/i18n.js b/src/web/public/i18n.js index 42657a23..6ce3396c 100644 --- a/src/web/public/i18n.js +++ b/src/web/public/i18n.js @@ -678,6 +678,9 @@ 'Claude Model': 'Claude 模型', '1M Opus Context': 'Opus 100 万上下文', 'Remote auto-reconnect': '远程自动重连', + 'Pin tab names on the CLI': '在 CLI 上固定标签名', + "Launch every Claude session with its tab name, so the CLI's prompt label and /resume list match your tabs. Claude then stops generating its own titles for those sessions, and a rename in Codeman holds only until the next turn. Applies from the next session start or respawn, no server restart needed.": + '以标签名启动每个 Claude 会话,让 CLI 的提示框标签和 /resume 列表与你的标签一致。Claude 随后不再为这些会话生成自己的标题,在 Codeman 中的重命名也只保留到下一轮。从下一次会话启动或重新生成时生效,无需重启服务器。', 'Thinking Effort': '思考强度', Low: '低', Medium: '中', diff --git a/test/pin-cli-session-name-i18n.test.ts b/test/pin-cli-session-name-i18n.test.ts new file mode 100644 index 00000000..74ce91db --- /dev/null +++ b/test/pin-cli-session-name-i18n.test.ts @@ -0,0 +1,66 @@ +/** + * @fileoverview The "Pin tab names on the CLI" App Settings row (#646) in Chinese (zh-CN): + * its label and description run through the real translator over the real index.html in + * JSDOM, read in Chinese with no English left beyond `CLI`, `Claude`, `Codeman` and + * `/resume`, and each key is in the dictionary once (a repeated key in the object literal + * silently replaces the first). + * + * Port: N/A. + */ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import vm from 'node:vm'; +import { JSDOM } from 'jsdom'; +import { afterAll, describe, expect, it } from 'vitest'; + +const PUBLIC = resolve(import.meta.dirname, '../src/web/public'); +const I18N = readFileSync(resolve(PUBLIC, 'i18n.js'), 'utf8'); +const INDEX = readFileSync(resolve(PUBLIC, 'index.html'), 'utf8'); + +type Api = { configure(o: Record): void; start(): void }; + +/** What may stay Latin: the product and CLI names and the slash command. */ +const leftover = (text: string) => text.replace(/\/resume|\b(CLI|Claude|Codeman)\b/g, '').match(/[A-Za-z]+/g) ?? []; + +function rowStrings(doc: Document): { label: string; description: string } { + const row = doc.getElementById('appSettingsPinCliSessionName')!.closest('.set-row')!; + return { + label: row.querySelector('.set-row-label')!.textContent!.trim(), + description: row.querySelector('.set-row-desc')!.textContent!.trim(), + }; +} + +const source = new JSDOM(INDEX); +const english = rowStrings(source.window.document); + +const dom = new JSDOM(INDEX, { runScripts: 'outside-only', url: 'http://localhost/' }); +vm.runInContext(I18N, dom.getInternalVMContext(), { filename: 'i18n.js' }); +const api = (dom.window as unknown as { CodemanI18n: Api }).CodemanI18n; +api.start(); +api.configure({ language: 'zh-CN' }); +const chinese = rowStrings(dom.window.document); + +afterAll(() => { + source.window.close(); + dom.window.close(); +}); + +describe('the Pin tab names on the CLI row in zh-CN', () => { + it('finds the row (the check is not vacuous)', () => { + expect(english.label).toBe('Pin tab names on the CLI'); + expect(english.description).toMatch(/^Launch every Claude session with its tab name/); + }); + + it('translates the label and the description, with no English left', () => { + for (const key of ['label', 'description'] as const) { + expect(chinese[key]).not.toBe(english[key]); + expect(leftover(chinese[key])).toEqual([]); + } + }); + + it('adds each key to the dictionary once', () => { + for (const text of [english.label, english.description]) { + expect(I18N.split(`'${text}'`).length + I18N.split(`"${text}"`).length - 2).toBe(1); + } + }); +});