Skip to content

fix(cli): show ⌥T instead of alt+T on macOS for thinking expansion - #5802

Merged
wenshao merged 5 commits into
QwenLM:mainfrom
Alex-ai-future:fix/alt-t
Jun 25, 2026
Merged

fix(cli): show ⌥T instead of alt+T on macOS for thinking expansion#5802
wenshao merged 5 commits into
QwenLM:mainfrom
Alex-ai-future:fix/alt-t

Conversation

@Alex-ai-future

Copy link
Copy Markdown
Contributor

What this PR does

Changes the thinking block expand/collapse keyboard shortcut hint from "alt+t" to "⌥t" on macOS, so the on-screen hint matches the actual key users need to press.

Why it's needed

The keybinding is defined as { key: 't', meta: true }, which maps to the Option (⌥) key on Mac. However, the UI always displayed "alt+t to expand/collapse" — macOS keyboards have no dedicated Alt key, so Mac users never knew which key to press. This PR detects process.platform === 'darwin' and shows the native ⌥ symbol instead.

Reviewer Test Plan

How to verify

  1. Start the CLI on macOS: npm run dev
  2. Ask a question that triggers a thinking block (e.g. "explain this code")
  3. While the thinking block is collapsed, verify the hint reads "⌥t to expand"
  4. Press Option+T and verify the thinking block expands
  5. Press Option+T again and verify it collapses, showing "⌥t to collapse"

Evidence (Before & After)

Before: (alt+t to expand) — confusing on Mac, no Alt key exists
After: (⌥t to expand) — matches the actual Option key on Mac keyboards

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Risk & Scope

  • Main risk or tradeoff: None — purely a display string change, no logic affected
  • Not validated / out of scope: Windows and Linux (they continue to show "alt+t" as before)
  • Breaking changes / migration notes: N/A

Linked Issues

N/A

中文说明

这个 PR 做了什么

在 macOS 上将思考块的展开/折叠快捷键提示从 "alt+t" 改为 "⌥t",使界面提示与实际需要按的键一致。

为什么需要

快捷键定义为 { key: 't', meta: true },在 Mac 上对应 Option (⌥) 键。但界面始终显示 "alt+t",而 Mac 键盘没有独立的 Alt 键,导致 Mac 用户不知道按哪个键。此 PR 检测 process.platform === 'darwin' 并显示原生的 ⌥ 符号。

审查者验证方案

如何验证

  1. 在 macOS 上启动 CLI:npm run dev
  2. 触发一个思考块(例如问"解释这段代码")
  3. 思考块折叠时,确认提示显示 "⌥t to expand"
  4. 按 Option+T,确认思考块展开
  5. 再按 Option+T,确认折叠并显示 "⌥t to collapse"

证据(Before & After)

Before: (alt+t to expand) — Mac 上令人困惑,不存在 Alt 键
After: (⌥t to expand) — 与 Mac 键盘上的 Option 键匹配

风险与范围

  • 主要风险或权衡:无 — 纯显示字符串变更,不影响逻辑
  • 未验证 / 超出范围:Windows 和 Linux(继续显示 "alt+t")
  • 破坏性变更 / 迁移说明:无

Alex-ai-future and others added 2 commits June 24, 2026 13:53
On macOS the Option key (⌥) maps to the meta keybinding, but the UI
hint displayed "alt+t" which is confusing since Mac keyboards have no
dedicated Alt key. Show the native ⌥ symbol on darwin so the on-screen
hint matches the actual key users need to press.
const durationSuffix =
durationMs != null ? ` ${formatDuration(durationMs)}` : '';

const toggleKeyHint = process.platform === 'darwin' ? '\u2325+t' : 'alt+t';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The Unicode (\u2325) symbol breaks the existing CLI key-hint convention. KeyboardShortcuts.tsx exclusively uses ASCII labels (alt+v, cmd+v, ctrl+o) — zero unicode modifier symbols anywhere in packages/cli/src/. This creates UI inconsistency: macOS users see ⌥+t in the thinking hint but alt+v in the keyboard shortcuts panel.

Additionally, the same toggleKeyHint expression is copy-pasted in 3 files (ConversationMessages.tsx, HistoryItemDisplay.test.tsx, ConversationMessages.test.tsx). If the hint text or platform logic ever changes, all 3 must be updated in lockstep.

Consider using alt+t on all platforms to match the existing convention, or extract this to a shared constant if introducing unicode symbols is intentional.

— qwen3.7-max via Qwen Code /review

<Text dimColor italic>
{THINKING_ICON}
{label} {t('(alt+t to expand)')}
{label} {t(`(${toggleKeyHint} to expand)`)}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The template literal inside t() produces two distinct translation keys per platform: "(⌥+t to expand)" on macOS and "(alt+t to expand)" elsewhere. When a translator later adds a locale entry for one key, the other platform silently falls back to English.

The t() function already supports {{param}} interpolation (i18n/index.ts:214-222). Use a stable key with a parameter:

Suggested change
{label} {t(`(${toggleKeyHint} to expand)`)}
{label} {t('({{keyHint}} to expand)', { keyHint: toggleKeyHint })}

This way translators work with one key ("({{keyHint}} to expand)") and the platform-specific value is injected at render time. Apply the same fix to the collapse variant below.

— qwen3.7-max via Qwen Code /review

- Use ASCII 'option+t' instead of Unicode '⌥t' to match the existing
  CLI key-hint convention (KeyboardShortcuts.tsx uses alt/cmd/ctrl
  strings exclusively)
- Use t() parameter interpolation instead of template literals so the
  translation key stays stable: t('({{keyHint}} to expand)', {keyHint})
  rather than generating separate keys per platform

Signed-off-by: Alex <alex.tech.lab@outlook.com>
@Alex-ai-future

Copy link
Copy Markdown
Contributor Author

Thanks for the review, @wenshao! Both points are valid — addressed:

**1. Unicode ⌥ symbol → ASCII **

You're right that uses ASCII exclusively for key hints (, , , etc.). I've changed the macOS hint from to to match the existing convention. No UI inconsistency now.

2. Dynamic translation keys → stable key with parameter interpolation

Good catch. Changed from:

t(`(${toggleKeyHint} to expand)`)  // produces two different keys per platform

to:

t('({{keyHint}} to expand)', { keyHint: toggleKeyHint })  // single stable key

Translators now work with one key and the platform-specific value is injected at render time. Applied to both expand and collapse variants.

All 35 tests pass.

const durationSuffix =
durationMs != null ? ` ${formatDuration(durationMs)}` : '';

const toggleKeyHint = process.platform === 'darwin' ? 'option+t' : 'alt+t';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] toggleKeyHint is declared inside the ThinkMessage component body and recomputed on every render. Both test files and KeyboardShortcuts.tsx define platform-aware key helpers (getNewlineKey, getPasteKey, etc.) at module scope.

Suggested change
const toggleKeyHint = process.platform === 'darwin' ? 'option+t' : 'alt+t';

Consider moving to module scope (e.g., right after THINKING_ICON) for consistency:

const toggleKeyHint = process.platform === 'darwin' ? 'option+t' : 'alt+t';

— qwen3.7-max via Qwen Code /review

import { render } from 'ink-testing-library';
import { ThinkMessage, ThinkMessageContent } from './ConversationMessages.js';

const toggleKeyHint = process.platform === 'darwin' ? 'option+t' : 'alt+t';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Tests mirror the component's process.platform conditional, so only the host platform's branch is ever exercised in CI. Other tests in the project (e.g., detect-terminal-theme.test.ts, clipboardUtils.test.ts) mock process.platform to cover both branches.

Consider adding a test that stubs process.platform to the opposite value and asserts the other key hint appears. This would catch a regression that hardcodes the wrong key on the non-host platform.

Additionally, the "should render full text when committed and expanded" test (line 37) asserts content is shown but doesn't verify the collapse hint text (to collapse). The collapse interpolation at ConversationMessages.tsx:376 is untested in this direct unit test — adding expect(output).toContain(\${toggleKeyHint} to collapse`)` would close that gap.

— qwen3.7-max via Qwen Code /review

useMouseEvents: vi.fn(),
}));

const toggleKeyHint = process.platform === 'darwin' ? 'option+t' : 'alt+t';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Same cross-platform test gap as ConversationMessages.test.tsx — this constant mirrors the component's conditional, so only the host platform's branch is exercised. Consider mocking process.platform to verify both branches.

— qwen3.7-max via Qwen Code /review

Placed next to THINKING_ICON constant for consistency with
KeyboardShortcuts.tsx patterns (getNewlineKey, getPasteKey at module
scope). Avoids recomputing on every render.

Signed-off-by: Alex <alex.tech.lab@outlook.com>
@Alex-ai-future

Copy link
Copy Markdown
Contributor Author

Thanks for the second round of review, @wenshao!

1. Move toggleKeyHint to module scope

Done — placed right after THINKING_ICON, consistent with KeyboardShortcuts.tsx patterns (getNewlineKey, getPasteKey at module scope).

2. Cross-platform test coverage

toggleKeyHint is a module-level constant evaluated once at import time, so Object.defineProperty(process, 'platform', ...) in beforeEach/afterEach can't change its value mid-test — the module is already loaded with the host platform's value. Other tests that mock process.platform (e.g. clipboardUtils.test.ts) use vi.resetModules() + dynamic import() because they export functions, not React components. Re-importing a React component module with JSX mid-test is not practical with the current test setup.

Given that toggleKeyHint is a single-line ternary with no branching logic beyond the platform check itself, the cross-platform test would only verify that process.platform === 'darwin' returns the expected string — which is essentially testing Node.js's platform detection, not our code. The existing tests cover the rendering paths (expand/collapse, i18n interpolation) on the host platform.

3. Collapse text assertion

The to collapse assertion was already present in the "renders committed thinking expanded when ThoughtExpandedProvider is true" test case — verified it's intact.


export const THINKING_ICON = '∴ ';

const toggleKeyHint = process.platform === 'darwin' ? 'option+t' : 'alt+t';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] toggleKeyHint is independently defined in 3 files — this source file plus both test files (ConversationMessages.test.tsx:10 and HistoryItemDisplay.test.tsx:32) each copy-paste the identical process.platform === 'darwin' ? 'option+t' : 'alt+t' expression. If the platform logic or label ever changes, it must be updated in 3 places, with silent divergence risk (a test file could pass on one platform and fail on another with a stale expectation).

Consider exporting the constant from this module (same pattern as THINKING_ICON) and importing it in the test files:

Suggested change
const toggleKeyHint = process.platform === 'darwin' ? 'option+t' : 'alt+t';
export const toggleKeyHint = process.platform === 'darwin' ? 'option+t' : 'alt+t';

Then in both test files:

import { toggleKeyHint } from './messages/ConversationMessages.js';

— qwen3.7-max via Qwen Code /review

Eliminates copy-paste duplication across 3 files. Tests now import
from the source module (same pattern as THINKING_ICON), so any future
change to the platform logic or label text only needs to be made once.

Signed-off-by: Alex <alex.tech.lab@outlook.com>
@Alex-ai-future

Copy link
Copy Markdown
Contributor Author

Good catch, @wenshao. Exported from and both test files now import it instead of copy-pasting the ternary. Single source of truth — future changes only need one edit. All 35 tests pass.

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found — clean, well-scoped display-string fix. LGTM.

⚠️ Downgraded from Approve to Comment: CI still running.

Verified at HEAD b82e70c: all three concerns from the earlier review rounds are resolved — ASCII option+t/alt+t (no Unicode), the constant lives at module scope, and it's a single exported source of truth imported by both test files.

The i18n change to t('({{keyHint}} to expand)', { keyHint }) is correct:

  • It uses the established {{var}} interpolation pattern already used across the codebase (t('Found {{count}} ...', { count }), etc.).
  • No check-i18n failure — the script only flags unused en.js keys as a warning and never errors on a source key absent from en.js; this hint was never an en.js key (neither before nor after).
  • No translation regression — the hint was never translated in any locale, so both the old and new keys fall back to the English source string identically.
  • The {' '} on the collapse line correctly preserves the space across the JSX line-wrap (without it, JSX would collapse the newline and drop the space).

Ran the two affected test files (ConversationMessages.test.tsx, HistoryItemDisplay.test.tsx): 35/35 pass.

中文版

未发现问题 —— 一个干净、范围明确的显示文案修复。可以合并。

⚠️ 已从 Approve 降级为 Comment:CI 仍在运行中。

在 HEAD b82e70c 上核验:前几轮 review 提出的三个问题都已解决 —— 改用 ASCII option+t/alt+t(不再用 Unicode)、常量提到模块作用域、并导出为唯一来源由两个测试文件 import。

i18n 改动 t('({{keyHint}} to expand)', { keyHint }) 是正确的:

  • 使用了代码库中既有的 {{var}} 插值模式(如 t('Found {{count}} ...', { count }))。
  • 不会导致 check-i18n 失败 —— 该脚本只把「en.js 里未被使用的 key」报为 warning,从不对「源码中存在但 en.js 没有的 key」报错;而这个提示文案本来就不是 en.js 的 key(改动前后都不是)。
  • 没有翻译回退退化 —— 该提示从未在任何语言包里被翻译过,新旧 key 都同样回退到英文源串。
  • collapse 行的 {' '} 正确地在 JSX 换行处保留了空格(否则 JSX 会折叠换行并丢掉空格)。

已运行两个受影响的测试文件(ConversationMessages.test.tsxHistoryItemDisplay.test.tsx):35/35 通过

— claude-opus-4-8 via Claude Code /qreview

@wenshao

wenshao commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

✅ Maintainer local verification — PR #5802

Built and tested the PR head (b82e70cc7) in a clean worktree (fresh npm ci + full build) on macOS (darwin, node v22.22.2). All CI gates and unit tests pass locally. One thing to reconcile before merge: the code ships option+t, but the title / description / "Evidence" all say ⌥t — they don't match what actually renders.

Results

Check Command Result
Typecheck tsc --noEmit (cli) ✅ pass (exit 0)
Lint eslint --max-warnings 0 (3 changed files) ✅ pass (exit 0)
Format prettier --check (3 changed files) ✅ All matched files use Prettier code style
i18n npm run check-i18n ✅ All checks passed
Build npm run build (full) ✅ pass (exit 0)
Bundle + boot npm run bundlenode dist/cli.js --version ✅ prints 0.19.1
Unit tests ConversationMessages.test.tsx + HistoryItemDisplay.test.tsx 35 passed

Real rendered output

Captured through ink-testing-library (the actual production render path of <ThinkMessage>), on macOS:

∴ Thought for 15s (option+t to expand)
∴ Thought for 15s (option+t to collapse)
  • ✅ The {{keyHint}} placeholder does not leak to the screen. t() runs interpolation even on the key-fallback path (translations[key] ?? key, then interpolate(...)), so the param is substituted correctly with no en.js entry required.
  • ⚠️ The string that ships is option+t, not ⌥t.

Findings

1. ⚠️ Code ships option+t, but the PR documents ⌥t — the one decision point

  • Source ConversationMessages.tsx:25-26: process.platform === 'darwin' ? 'option+t' : 'alt+t'
  • PR title: "show ⌥T instead of alt+T"; body: "shows the native symbol"; Evidence: "After: (⌥t to expand)"
  • Actual render (above): (option+t to expand)

Both option+t and ⌥t are reasonable labels, but the PR's own Before & After evidence does not match the shipped behavior. Please pick one and make code + description agree:

  • keep option+t → update the title / description / Evidence to say option+t; or
  • you actually want the glyph → change the constant to '⌥t' (or '⌥T').

2. ✅ i18n is correct, but the hint stays English-only (not a regression)

The new keys ({{keyHint}} to expand) / ({{keyHint}} to collapse) are not added to any locales/*.js — neither was the old (alt+t to expand) — so on every UI language the hint renders from the English key via fallback, exactly as before this PR. check-i18n is green because "used key missing from en.js" is not an error in this repo's checker. No action needed unless you want this hint localized.

3. ✅ Tests are platform-adaptive and guard the leak

Both test files import the shared toggleKeyHint and assert `${toggleKeyHint} to expand/collapse`, so they pass on macOS (option+t) and on Linux CI (alt+t) alike, and they would fail if interpolation ever regressed to a literal {{keyHint}}. Sound approach.

4. ℹ️ Pre-existing (not changed by this PR): the binding only fires on Option when the terminal maps Option→Meta

TOGGLE_THINKING_EXPANDED = [{ key: 't', meta: true }]. On macOS this is reached only when the terminal sends Option-as-Meta (iTerm2 "Esc+", Terminal.app "Use Option as Meta Key"); in stock Terminal.app, Option+T types and won't toggle. This PR only relabels the hint and leaves the binding untouched, so it's out of scope — flagging only because the label now explicitly promises "option+t".

5. ✅ Non-macOS unchanged — Linux/Windows still render alt+t (the branch is present in the shipped dist/cli.js).

Recommendation

Functionally safe and every gate is green. Recommend merging once the option+t vs ⌥t mismatch is reconciled (a one-line change either way). Everything else looks good. 👍

🔁 Reproduction
git fetch origin pull/5802/head:pr-5802
git worktree add ../wt-pr5802 pr-5802
cd ../wt-pr5802 && npm ci && npm --prefix packages/core run build
# unit tests (the two changed files)
( cd packages/cli && npx vitest run \
    src/ui/components/messages/ConversationMessages.test.tsx \
    src/ui/components/HistoryItemDisplay.test.tsx )
# gates
( cd packages/cli && npx tsc --noEmit )
npm run check-i18n
npx prettier --check packages/cli/src/ui/components/messages/ConversationMessages.tsx
npm run build && npm run bundle && node dist/cli.js --version
🇨🇳 中文版(完整对应)

✅ 维护者本地验证 — PR #5802

独立 worktree(全新 npm ci + 完整 build)中,于 macOSdarwin,node v22.22.2)构建并测试了 PR head(b82e70cc7)。本地所有 CI 关卡与单元测试均通过。合并前需要对齐一处: 代码实际输出 option+t,但标题 / 描述 / "Evidence" 都写的是 ⌥t,与真实渲染不一致。

结果

检查 命令 结果
类型检查 tsc --noEmit(cli) ✅ 通过(exit 0)
Lint eslint --max-warnings 0(3 个改动文件) ✅ 通过(exit 0)
格式 prettier --check(3 个改动文件) ✅ 全部符合 Prettier 风格
i18n npm run check-i18n ✅ All checks passed
构建 npm run build(全量) ✅ 通过(exit 0)
打包 + 启动 npm run bundlenode dist/cli.js --version ✅ 输出 0.19.1
单元测试 ConversationMessages.test.tsx + HistoryItemDisplay.test.tsx 35 passed

真实渲染输出

通过 ink-testing-library<ThinkMessage> 的真实生产渲染路径)在 macOS 上捕获:

∴ Thought for 15s (option+t to expand)
∴ Thought for 15s (option+t to collapse)
  • {{keyHint}} 占位符没有泄漏到屏幕。t() 即使在 key 回退路径(translations[key] ?? key 后再 interpolate(...))也会做插值,因此参数被正确替换,无需 en.js 条目。
  • ⚠️ 实际输出的字符串是 option+t,不是 ⌥t

发现

1. ⚠️ 代码输出 option+t,但 PR 文档写的是 ⌥t —— 唯一需要决策的点

  • 源码 ConversationMessages.tsx:25-26: process.platform === 'darwin' ? 'option+t' : 'alt+t'
  • PR 标题:"show ⌥T instead of alt+T";正文:"shows the native symbol";Evidence:"After: (⌥t to expand)"
  • 实际渲染(见上):(option+t to expand)

option+t⌥t 都是合理的标签,但 PR 自己的 Before & After 证据与实际行为不符。请二选一,让代码与描述一致:

  • 保留 option+t → 把标题 / 描述 / Evidence 改成 option+t;
  • 确实想要符号 → 把常量改成 '⌥t'(或 '⌥T')。

2. ✅ i18n 实现正确,但提示仍为纯英文(非回归)

新增的 key ({{keyHint}} to expand) / ({{keyHint}} to collapse) 没有加入任何 locales/*.js——旧的 (alt+t to expand) 同样没有——所以在所有 UI 语言下,该提示都通过英文 key 回退渲染,与本 PR 之前完全一致。check-i18n 通过,是因为本仓库的检查器并不把"源码用到但 en.js 缺失的 key"视为错误。除非你想本地化这条提示,否则无需处理。

3. ✅ 测试随平台自适应,并能挡住占位符泄漏

两个测试文件都导入共享的 toggleKeyHint 并断言 `${toggleKeyHint} to expand/collapse`,因此在 macOS(option+t)和 Linux CI(alt+t)上都通过;一旦插值退化成字面 {{keyHint}},测试就会失败。设计稳妥。

4. ℹ️ 既有问题(非本 PR 引入):仅当终端把 Option 映射为 Meta 时,该绑定才会触发

TOGGLE_THINKING_EXPANDED = [{ key: 't', meta: true }]。在 macOS 上,只有当终端发送 Option-as-Meta(iTerm2 "Esc+"、Terminal.app "Use Option as Meta Key")时才会命中;在原生 Terminal.app 默认配置下,Option+T 会输入 而不会触发切换。本 PR 只改提示文案、未动绑定,所以这超出范围——之所以提一句,是因为文案现在明确承诺了 "option+t"。

5. ✅ 非 macOS 不变 —— Linux/Windows 仍渲染 alt+t(该分支存在于产出的 dist/cli.js 中)。

建议

功能上安全,所有关卡全绿。建议在对齐 option+t⌥t 的不一致后合并(两种改法都只是一行)。其余一切良好。👍

Verified locally by the maintainer on macOS. Static gates + 35 unit tests green; the only open item is the option+t / ⌥t wording mismatch.

@doudouOUC doudouOUC left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found. LGTM! ✅

— qwen3.7-max via Qwen Code /review

@wenshao
wenshao added this pull request to the merge queue Jun 25, 2026
Merged via the queue into QwenLM:main with commit 0ac99f0 Jun 25, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants