Skip to content

fix(cli): use high-contrast software cursor - #5720

Merged
wenshao merged 1 commit into
QwenLM:mainfrom
tt-a1i:fix/high-contrast-software-cursor
Jun 23, 2026
Merged

fix(cli): use high-contrast software cursor#5720
wenshao merged 1 commit into
QwenLM:mainfrom
tt-a1i:fix/high-contrast-software-cursor

Conversation

@tt-a1i

@tt-a1i tt-a1i commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Replaces the input software cursor's terminal-dependent reverse-video styling with a shared high-contrast cursor renderer. The renderer chooses a light or dark cursor background from the active theme background, so dark themes such as Solarized/Alacritty get a visible light cursor while light themes get a visible dark cursor.

The shared renderer is used by the main prompt, the base text input, the legacy shared text input, the sensitive setting prompt, and the settings dialog edit buffer. Existing line-end zero-width-space behavior is preserved so trailing cursor spaces are not trimmed by Ink.

Why it's needed

Some terminals and color palettes render chalk.inverse() with very low contrast. In the reported Alacritty setup, the cursor becomes semi-invisible because reverse video depends on how the terminal swaps the current foreground and background colors.

Using an explicit, theme-aware cursor background makes the software cursor visible without adding terminal-specific branches.

Reviewer Test Plan

How to verify

  1. Open Qwen Code in a terminal/theme where the input cursor was hard to see, such as Alacritty with a dark Solarized-style palette.
  2. Focus the main input prompt and move the cursor through normal text, token-highlighted text, Unicode text, and the end of a line.
  3. Confirm the cursor is a visible block in those positions and that typing/navigation behavior is unchanged.
  4. Optionally switch to a light theme and confirm the cursor remains visible there too.

Evidence (Before & After)

Before: the software cursor used chalk.inverse(), which emits reverse-video styling and can be low-contrast in Alacritty palettes.

After: cursor rendering uses renderSoftwareCursor(), which emits an explicit theme-aware background and unit tests assert it does not use SGR 7 reverse video or reset surrounding foreground color.

Local validation:

$ npm test --workspace=packages/cli -- ui/utils/software-cursor.test.ts ui/components/InputPrompt.test.tsx ui/components/BaseTextInput.test.tsx ui/components/shared/TextInput.test.tsx ui/components/SettingInputPrompt.test.tsx --coverage.enabled=false
Test Files  5 passed (5)
Tests  202 passed (202)

$ npx eslint packages/cli/src/ui/utils/software-cursor.ts packages/cli/src/ui/utils/software-cursor.test.ts packages/cli/src/ui/components/BaseTextInput.tsx packages/cli/src/ui/components/BaseTextInput.test.tsx packages/cli/src/ui/components/InputPrompt.tsx packages/cli/src/ui/components/InputPrompt.test.tsx packages/cli/src/ui/components/SettingInputPrompt.tsx packages/cli/src/ui/components/SettingsDialog.tsx packages/cli/src/ui/components/shared/TextInput.tsx packages/cli/src/ui/components/shared/TextInput.test.tsx

$ npx prettier --check packages/cli/src/ui/utils/software-cursor.ts packages/cli/src/ui/utils/software-cursor.test.ts packages/cli/src/ui/components/BaseTextInput.tsx packages/cli/src/ui/components/BaseTextInput.test.tsx packages/cli/src/ui/components/InputPrompt.tsx packages/cli/src/ui/components/InputPrompt.test.tsx packages/cli/src/ui/components/SettingInputPrompt.tsx packages/cli/src/ui/components/SettingsDialog.tsx packages/cli/src/ui/components/shared/TextInput.tsx packages/cli/src/ui/components/shared/TextInput.test.tsx
All matched files use Prettier code style!

$ npm run typecheck --workspace=packages/cli --if-present
tsc --noEmit

$ git diff --check upstream/main..HEAD

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested locally
🐧 Linux ⚠️ not tested locally

Environment (optional)

Local validation used Node v26.3.0 and npm 11.16.0.

Risk & Scope

  • Main risk or tradeoff: the cursor now uses an explicit background color instead of terminal reverse video, so it may look slightly different from the previous terminal-native inverse style.
  • Not validated / out of scope: I did not run a live Alacritty capture locally; the change is validated with focused rendering tests and the reported root cause.
  • Breaking changes / migration notes: none.

Linked Issues

Fixes #5713

AI Assistance Disclosure

I used Codex to review the changes, sanity-check the implementation against existing patterns, and help spot potential edge cases.

中文说明

What this PR does

这个 PR 将输入框的软件光标从依赖终端反色视频的样式,改为共享的高对比光标渲染器。渲染器会根据当前主题背景选择浅色或深色光标背景,因此像 Solarized/Alacritty 这类深色主题会得到可见的浅色光标,浅色主题会得到可见的深色光标。

共享渲染器用于主输入框、基础文本输入框、旧的共享文本输入框、敏感设置输入框,以及设置对话框的编辑缓冲区。已有的行尾零宽空格行为保持不变,避免 Ink 裁掉行尾光标空格。

Why it's needed

某些终端和配色方案会把 chalk.inverse() 渲染成很低的对比度。在本 issue 报告的 Alacritty 配置里,光标会变得半透明/不明显,因为反色视频依赖终端如何交换当前前景色和背景色。

使用明确的、跟随主题背景选择的光标背景,可以让软件光标保持可见,同时不需要加入终端特定分支。

Reviewer Test Plan

How to verify

  1. 在之前光标不明显的终端/主题中打开 Qwen Code,例如使用深色 Solarized 风格配色的 Alacritty。
  2. 聚焦主输入框,并把光标移动到普通文本、带 token 高亮的文本、Unicode 文本以及行尾。
  3. 确认这些位置的光标都是可见的块状光标,并且输入/导航行为没有变化。
  4. 可选:切换到浅色主题,确认光标仍然可见。

Evidence (Before & After)

Before:软件光标使用 chalk.inverse(),会发出反色视频样式,在 Alacritty 配色下可能低对比。

After:光标渲染改为 renderSoftwareCursor(),它会发出明确的、跟随主题选择的背景色;单元测试断言它不会使用 SGR 7 反色视频,也不会重置外层前景色。

本地验证:

$ npm test --workspace=packages/cli -- ui/utils/software-cursor.test.ts ui/components/InputPrompt.test.tsx ui/components/BaseTextInput.test.tsx ui/components/shared/TextInput.test.tsx ui/components/SettingInputPrompt.test.tsx --coverage.enabled=false
Test Files  5 passed (5)
Tests  202 passed (202)

$ npx eslint packages/cli/src/ui/utils/software-cursor.ts packages/cli/src/ui/utils/software-cursor.test.ts packages/cli/src/ui/components/BaseTextInput.tsx packages/cli/src/ui/components/BaseTextInput.test.tsx packages/cli/src/ui/components/InputPrompt.tsx packages/cli/src/ui/components/InputPrompt.test.tsx packages/cli/src/ui/components/SettingInputPrompt.tsx packages/cli/src/ui/components/SettingsDialog.tsx packages/cli/src/ui/components/shared/TextInput.tsx packages/cli/src/ui/components/shared/TextInput.test.tsx

$ npx prettier --check packages/cli/src/ui/utils/software-cursor.ts packages/cli/src/ui/utils/software-cursor.test.ts packages/cli/src/ui/components/BaseTextInput.tsx packages/cli/src/ui/components/BaseTextInput.test.tsx packages/cli/src/ui/components/InputPrompt.tsx packages/cli/src/ui/components/InputPrompt.test.tsx packages/cli/src/ui/components/SettingInputPrompt.tsx packages/cli/src/ui/components/SettingsDialog.tsx packages/cli/src/ui/components/shared/TextInput.tsx packages/cli/src/ui/components/shared/TextInput.test.tsx
All matched files use Prettier code style!

$ npm run typecheck --workspace=packages/cli --if-present
tsc --noEmit

$ git diff --check upstream/main..HEAD

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested locally
🐧 Linux ⚠️ not tested locally

Environment (optional)

本地验证使用 Node v26.3.0 和 npm 11.16.0。

Risk & Scope

  • 主要风险或取舍:光标现在使用明确的背景色,而不是终端反色视频,因此视觉上可能和之前的终端原生反色略有不同。
  • 未验证 / 不在范围内:我没有在本地录制 Alacritty 实机画面;这个改动通过聚焦的渲染测试和已确认的根因来验证。
  • 破坏性变更 / 迁移说明:无。

Linked Issues

Fixes #5713

AI Assistance Disclosure

I used Codex to review the changes, sanity-check the implementation against existing patterns, and help spot potential edge cases.

@tt-a1i
tt-a1i marked this pull request as ready for review June 23, 2026 01:59
@wenshao

wenshao commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR, @tt-a1i!

Template looks good ✓

On direction: this fixes a real user-reported problem — the software cursor being semi-invisible in Alacritty with Solarized palettes (#5713). Terminal cursor visibility is squarely within the CLI's core UX, and the issue has clear reproduction evidence. Aligned.

On approach: the scope is tight and focused. One new utility (renderSoftwareCursor), mechanical replacement across the five call sites, and tests that assert the new behavior (no SGR 7, no foreground reset). No drive-by refactors or scope creep. One minor note: toHex() and the INK_NAME_TO_HEX map in software-cursor.ts duplicate identical code already in color-utils.ts — exporting toHex from there would eliminate the duplication. Not a blocker, but worth considering.

Moving on to code review and testing. 🔍

中文说明

感谢贡献,@tt-a1i

模板完整 ✓

方向:这个 PR 修复了用户实际报告的问题 —— 在 Alacritty + Solarized 配色下软件光标几乎不可见(#5713)。终端光标可见性属于 CLI 核心 UX 范畴,issue 也有清晰的复现证据。方向对齐。

方案:范围紧凑、专注。一个新工具函数(renderSoftwareCursor),五个调用点的机械替换,以及验证新行为的测试(无 SGR 7、无前景色重置)。没有顺手重构或范围蔓延。一个小备注:software-cursor.ts 中的 toHex()INK_NAME_TO_HEX 映射与 color-utils.ts 中已有的代码完全重复 —— 如果从那边导出 toHex 可以消除重复。不是阻塞项,但值得考虑。

进入代码审查和测试 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

The implementation is clean and well-focused. A quick summary:

  • renderSoftwareCursor() is a small, self-contained utility that replaces chalk.inverse() with an explicit chalk.bgHex() call. The luminance check (BT.601 formula) correctly picks a light or dark cursor background based on the theme.
  • All five call sites (BaseTextInput, InputPrompt, SettingInputPrompt, SettingsDialog, shared TextInput) are updated mechanically — chalk.inverse(x)renderSoftwareCursor(x). No logic changes, no drive-by refactors.
  • One minor reuse note: toHex() and the INK_NAME_TO_HEX map in software-cursor.ts duplicate identical code already present in color-utils.ts. Exporting the existing toHex from there would remove ~40 lines of duplication. Not a blocker.
  • The theme.background?.primary access is valid per the SemanticColors type definition.
  • Tests assert the important invariants: no SGR 7 (reverse video), no \e[39m (foreground reset), correct light/dark background selection, named color handling, and empty-string fallback.

No correctness bugs, no security issues, no regressions found.

Test Results

Unit tests (3/5 test files pass, 19/19 tests):

 ✓ src/ui/utils/software-cursor.test.ts (7 tests) 8ms
 ✓ src/ui/components/shared/TextInput.test.tsx (6 tests) 60ms
 ✓ src/ui/components/SettingInputPrompt.test.tsx (6 tests) 139ms

 Test Files  3 passed (3)
      Tests  19 passed (19)

The remaining 2 test files (BaseTextInput, InputPrompt) fail with a pre-existing ink/dom import resolution error that also affects main — this is an environment issue, not caused by the PR.

tmux smoke test (installed qwen v0.18.5 — before):

runner@runnervm7b5n9:~/work/qwen-code/qwen-code$ qwen --version 2>&1
0.18.5
runner@runnervm7b5n9:~/work/qwen-code/qwen-code$ qwen -p 'What is 2+2? Answer in one word.' 2>&1 | head -5
Four
runner@runnervm7b5n9:~/work/qwen-code/qwen-code$ qwen -p 'say hello' 2>&1
Hello! How can I help you today?
runner@runnervm7b5n9:~/work/qwen-code/qwen-code$

tmux smoke test (after — PR dev build): could not run — the dev build fails with a pre-existing ink/dom module resolution error that also breaks npm run build on main in this CI environment. The ink 7.0.3 package exports don't include ./dom or ./components/CursorContext subpaths. This is an environment limitation, not a PR defect.

The cursor rendering change is purely visual (ANSI escape codes), so tmux capture-pane wouldn't show the difference regardless. The unit tests, which assert the correct escape codes (\e[48;2;...m instead of \e[7m), are the meaningful verification here — and they all pass.

中文说明

代码审查

实现干净、专注。简要总结:

  • renderSoftwareCursor() 是一个小型、独立的工具函数,用 chalk.bgHex() 替代 chalk.inverse()。亮度检测(BT.601 公式)根据主题正确选择浅色或深色光标背景。
  • 五个调用点(BaseTextInput、InputPrompt、SettingInputPrompt、SettingsDialog、shared TextInput)均为机械替换 —— chalk.inverse(x)renderSoftwareCursor(x)。无逻辑变更,无顺手重构。
  • 一个小复用备注:software-cursor.ts 中的 toHex()INK_NAME_TO_HEX 映射与 color-utils.ts 中已有代码完全重复。从那边导出已有的 toHex 可以减少约 40 行重复代码。非阻塞项。
  • theme.background?.primary 访问在 SemanticColors 类型定义中有效。
  • 测试断言了重要不变量:无 SGR 7(反色视频)、无 \e[39m(前景色重置)、正确的深/浅背景选择、命名颜色处理、空字符串回退。

未发现正确性 bug、安全问题或回归。

测试结果

单元测试(3/5 测试文件通过,19/19 测试用例):

 ✓ src/ui/utils/software-cursor.test.ts (7 tests) 8ms
 ✓ src/ui/components/shared/TextInput.test.tsx (6 tests) 60ms
 ✓ src/ui/components/SettingInputPrompt.test.tsx (6 tests) 139ms

其余 2 个测试文件(BaseTextInput、InputPrompt)因 ink/dom 导入解析错误而失败 —— 这是影响 main 分支的预存环境问题,非 PR 引入。

tmux 冒烟测试(已安装 qwen v0.18.5 —— before):正常运行。PR dev build(after)因预存的 ink/dom 环境限制无法运行,非 PR 缺陷。

光标渲染变更是纯视觉的(ANSI 转义码),tmux capture-pane 本身无法显示差异。单元测试断言了正确的转义码(\e[48;2;...m 而非 \e[7m),是有意义的验证 —— 全部通过。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Final Assessment

This is a straightforward, well-executed bugfix. The motivation is a real user-reported issue (#5713 — invisible cursor in Alacritty + Solarized), the implementation is the minimal change needed (one utility function, mechanical call-site updates), and the tests assert the right things (no reverse video, no foreground reset, correct luminance-based background selection).

Looking back at my independent proposal from Stage 2a: I would have done the same thing — replace chalk.inverse() with an explicit chalk.bgHex() driven by a luminance check. The PR's implementation matches that. Nothing simpler was missed.

The only flag I raised — toHex() / INK_NAME_TO_HEX duplication with color-utils.ts — is a minor cleanup opportunity, not a reason to hold this up.

Verdict: Approve ✅

中文说明

最终评估

这是一个简洁、执行良好的 bugfix。动机来自用户实际报告的问题(#5713 —— Alacritty + Solarized 下光标不可见),实现是所需的最小变更(一个工具函数,机械的调用点替换),测试断言了正确的行为(无反色视频、无前景色重置、正确的亮度背景选择)。

回顾 Stage 2a 的独立方案:我会做同样的事 —— 用亮度检测驱动的 chalk.bgHex() 替代 chalk.inverse()。PR 的实现与此一致,没有遗漏更简路径。

唯一提到的问题 —— toHex() / INK_NAME_TO_HEXcolor-utils.ts 的重复 —— 是一个小的清理机会,不是阻止合并的理由。

结论:批准 ✅

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot 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.

LGTM, looks ready to ship. ✅

@wenshao

wenshao commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

✅ Real-TUI verification report (local, tmux)

I built the real qwen binary from this PR's head in an isolated git worktree and drove it in tmux with truecolor, capturing the cursor cell's raw ANSI via tmux capture-pane -e. Three layers: a real-TTY A/B, a function-level A/B, and a mutation test of the added unit tests.

Verdict: the fix is correct and resolves #5713 — safe to merge. One non-blocking note on test quality at the end.

1. Decisive proof — real TTY (this is what actually validates the fix)

Same keystrokes on both builds (type CURSORZZZ in the main composer), then capture the cursor cell with escapes:

build cursor cell SGR reverse \e[7m explicit bg \e[48;2;212;212;212m
PRE-FIX (chalk.inverse) \e[7m reverse video 1 0
FIXED (renderSoftwareCursor) \e[48;2;212;212;212m (#D4D4D4) 0 1

Captured cursor cell (ESC shown as \e, trailing spaces trimmed):

PRE-FIX : …\e[48;2;30;30;46m CURSORZZZ \e[7m ·​ \e[0m…      ← cursor = reverse-video space (terminal-dependent)
FIXED   : …\e[48;2;30;30;46m CURSORZZZ \e[48;2;212;212;212m ·​ …  ← cursor = explicit #D4D4D4 background space

So the cursor genuinely switches from terminal-dependent reverse-video (the #5713 low-contrast root cause in Alacritty/Solarized) to an explicit, theme-aware high-contrast background. Truecolor survives through tmux; on the dark default theme (bg #1E1E2E, luminance < 128) it correctly picks the light cursor #D4D4D4. The (ZWSP) anti-trim guard is preserved.

2. Color-choice logic — function-level A/B (FORCE_COLOR=3)

Importing the built software-cursor.js directly:

  • OLD chalk.inverse('X')\e[7mX\e[27m (reverse)
  • NEW renderSoftwareCursor('X')\e[48;2;212;212;212mX\e[49m (explicit bg)

getSoftwareCursorBackground() luminance branch verified across cases: dark bg → light cursor #D4D4D4; light bg → dark cursor #3A3A3A; boundary #808080 (lum == 128) → dark; named colors via the INK map (white→dark, black→light); 3-digit hex expansion (#fff→dark); unknown / empty → light default. All correct.

3. Mutation test — added unit tests are vacuous w.r.t. the styling switch (non-blocking)

The decisive proof above is the real-TTY A/B, not the unit tests, because the unit tests can't see color:

  • Probe: in the vitest env chalk.level === 0, so renderSoftwareCursor('x') and chalk.inverse('x') both return bare "x" (no SGR at all).
  • Mutation A — revert all 5 component sources to base (chalk.inverse), keep the PR tests: BaseTextInput.test.tsx (5), InputPrompt.test.tsx › Highlighting and Cursor Display (12), shared/TextInput.test.tsx (6), SettingInputPrompt.test.tsx (6) all still pass.
  • Mutation B — change renderSoftwareCursor back to chalk.inverse: software-cursor.test.ts still passes 7/7, including "uses an explicit background instead of reverse-video styling" (expect(rendered).not.toContain('�[7m')).

Reason: with chalk.level === 0 the styling collapses to plain text, so every assertion that builds its expectation through renderSoftwareCursor(...) (and the not.toContain('�[7m') check) passes for both implementations. Only the 4 getSoftwareCursorBackground(...) assertions are non-vacuous (they test pure logic, independent of chalk). FORCE_COLOR=1 is set only for the integration-tests/ terminal-capture e2e — not for these unit tests — and this PR adds no integration test for the cursor.

Optional suggestion (not a merge blocker): to actually guard against a regression back to reverse-video, force a color level in the styling tests — e.g. assert against new chalk.Instance({ level: 3 }) output, or set chalk.level = 3 in the test — so renderSoftwareCursor emits real SGR and not.toContain('�[7m') / toContain('[48;2;') become meaningful.

Environment

Real qwen binary built from PR head (70ef0af) via npm ci && npm run build; tmux 3.6a, truecolor (terminal-overrides ',*:Tc'), isolated HOME; macOS, Node v22. A/B done by reverting only the relevant source to origin/main and rebuilding the same worktree.

中文版(完整对应)

✅ 真实 TUI 验证报告(本地 tmux)

我在隔离的 git worktree 里用本 PR head 构建了真实的 qwen 二进制,并在 tmux(开真彩)里驱动它,用 tmux capture-pane -e 抓取光标单元格的原始 ANSI。三层验证:真实 TTY A/B、函数级 A/B、以及对新增单测的变异测试。

结论:修复正确,解决了 #5713 —— 可以合并。 末尾有一条不阻塞合并的测试质量提示。

1. 决定性证据 —— 真实 TTY(这才是真正验证修复的部分)

两个构建跑相同按键(在主输入框输入 CURSORZZZ),再带转义抓光标格:

构建 光标格 SGR 反色 \e[7m 显式背景 \e[48;2;212;212;212m
PRE-FIXchalk.inverse \e[7m 反色 1 0
FIXEDrenderSoftwareCursor \e[48;2;212;212;212m(#D4D4D4) 0 1

抓到的光标格(ESC 显示为 \e,已裁掉行尾空格):

PRE-FIX : …\e[48;2;30;30;46m CURSORZZZ \e[7m ·​ \e[0m…      ← 光标 = 反色空格(依赖终端)
FIXED   : …\e[48;2;30;30;46m CURSORZZZ \e[48;2;212;212;212m ·​ …  ← 光标 = 显式 #D4D4D4 背景空格

可见光标确实从"依赖终端的反色视频"(#5713 在 Alacritty/Solarized 下低对比的根因)切换为"显式、跟随主题的高对比背景"。真彩透过 tmux 完整保留;在暗色默认主题(背景 #1E1E2E,亮度 < 128)下正确选了浅色光标 #D4D4D4。代码里的 (零宽空格)防裁剪保护也保留了。

2. 选色逻辑 —— 函数级 A/B(FORCE_COLOR=3

直接 import 构建后的 software-cursor.js

  • chalk.inverse('X')\e[7mX\e[27m(反色)
  • renderSoftwareCursor('X')\e[48;2;212;212;212mX\e[49m(显式背景)

getSoftwareCursorBackground() 的亮度分支逐例验证:暗背景 → 浅光标 #D4D4D4;亮背景 → 深光标 #3A3A3A;边界 #808080(亮度 == 128)→ 深;命名色走 INK 映射(white→深、black→浅);三位十六进制展开(#fff→深);未知 / 空 → 默认浅色。全部正确。

3. 变异测试 —— 新增单测对"样式切换"是空过的(不阻塞合并)

上面的决定性证据来自真实 TTY A/B,而不是单测,因为单测看不到颜色:

  • 探针: vitest 环境里 chalk.level === 0,所以 renderSoftwareCursor('x')chalk.inverse('x') 返回裸 "x"(完全没有 SGR)。
  • Mutation A —— 把 5 个组件源码全部回退到 base(chalk.inverse),保留 PR 的测试:BaseTextInput.test.tsx(5)、InputPrompt.test.tsx › Highlighting and Cursor Display(12)、shared/TextInput.test.tsx(6)、SettingInputPrompt.test.tsx(6) 依旧全部通过
  • Mutation B —— 把 renderSoftwareCursor 改回 chalk.inversesoftware-cursor.test.ts 仍 7/7 通过,包括 "uses an explicit background instead of reverse-video styling"expect(rendered).not.toContain('�[7m'))。

原因:chalk.level === 0 时样式退化为纯文本,于是所有"用 renderSoftwareCursor(...) 构造期望串"的断言、以及 not.toContain('�[7m'),对两种实现都成立。只有那 4 个 getSoftwareCursorBackground(...) 断言是非空过的(测纯逻辑,与 chalk 无关)。FORCE_COLOR=1 只在 integration-tests/ 的 terminal-capture e2e 里设置 —— 这些单测里没有 —— 而本 PR 没有为光标新增任何 integration test。

可选建议(不阻塞合并): 想真正防住"回退到反色"的回归,可在样式测试里强制一个 color level —— 例如断言 new chalk.Instance({ level: 3 }) 的输出,或在测试里设 chalk.level = 3 —— 这样 renderSoftwareCursor 才会发出真实 SGR,not.toContain('�[7m') / toContain('[48;2;') 才有意义。

环境

真实 qwen 二进制由 PR head(70ef0af)经 npm ci && npm run build 构建;tmux 3.6a、真彩(terminal-overrides ',*:Tc')、隔离 HOME;macOS、Node v22。A/B 通过只把相关源码回退到 origin/main 并在同一 worktree 重建得到。

@wenshao
wenshao merged commit 0b2c9d0 into QwenLM:main Jun 23, 2026
38 checks passed
@wenshao

wenshao commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

✅ Independent re-confirmation — raw app bytes + live light-theme flip

Complements the thorough real-TTY / function-level / mutation report above — same verdict, safe to merge. Two things this run adds rather than repeats:

1. Captured the raw app output (not a screen reconstruction). Instead of tmux capture-pane -e (tmux's redraw of the cell), I used tmux pipe-pane to log the exact bytes the qwen process writes to the PTY, with FORCE_COLOR=3. So these SGRs are what the app itself emits — not tmux's interpretation. Typed hello in the main composer; cursor sits on the trailing space:

build raw cursor bytes (ESC → \e) reverse \e[7m explicit bg
BASE (chalk.inverse) hello\e[7m \e[27m 2 0
FIXED · Qwen Dark (default) hello\e[48;2;212;212;212m \e[48;2;30;30;46m 0 #D4D4D4
FIXED · Qwen Light hello\e[48;2;58;58;58m \e[48;2;250;250;250m 0 #3A3A3A

2. The theme-awareness flip, end-to-end in the live TUI. Rather than only computing getSoftwareCursorBackground() at the function level, I switched themes in the running app/themeQwen Light → Enter — then re-captured. The cursor background flips from light #D4D4D4 (on the dark default) to dark #3A3A3A (on Qwen Light), each high-contrast against its own theme background (\e[48;2;30;30;46m = #1E1E2E dark vs \e[48;2;250;250;250m = #FAFAFA light). So the luminance branch is wired correctly through the real theme pipeline, not just in isolation.

Net: reverse-video → explicit, theme-aware background is confirmed at the real surface from a second capture method, and the dark/light flip is real end-to-end. Concur with LGTM. (No new concerns; I agree with the note above that the added vitest cases are color-blind under chalk.level === 0 and don't by themselves guard the reverse-video regression — forcing a color level in those tests would close that gap.)

BASE = current main-line cursor code (chalk.inverse, verified inverse-only in src+built dist, no software-cursor); FIXED = PR head built in an isolated worktree. macOS, tmux, FORCE_COLOR=3, raw pipe-pane capture.

🇨🇳 中文版

✅ 独立复核 —— 原始 app 字节 + 实时浅色主题翻转

作为上面那份「真实 TTY / 函数级 / 变异测试」报告的补充 —— 结论一致,可以合并。这一轮新增(而非重复)两点:

1. 抓的是 app 的原始输出(不是屏幕重建)。 我没用 tmux capture-pane -e(那是 tmux 对单元格的重绘),而是用 tmux pipe-pane 记录 qwen 进程写到 PTY 的确切字节,并设 FORCE_COLOR=3。所以这些 SGR 是 app 自己发出的 —— 不是 tmux 的解释。在主输入框输入 hello,光标落在行尾空格:

构建 原始光标字节(ESC → \e 反色 \e[7m 显式背景
BASEchalk.inverse hello\e[7m \e[27m 2 0
FIXED · Qwen Dark(默认) hello\e[48;2;212;212;212m \e[48;2;30;30;46m 0 #D4D4D4
FIXED · Qwen Light hello\e[48;2;58;58;58m \e[48;2;250;250;250m 0 #3A3A3A

2. 主题自适应翻转,在实时 TUI 里端到端验证。 我没有只在函数级算 getSoftwareCursorBackground(),而是在运行中的 app 里切主题 —— /themeQwen Light → 回车 —— 再重新抓。光标背景从浅色 #D4D4D4(暗色默认主题)翻转为深色 #3A3A3A(Qwen Light),各自相对自己的主题背景都是高对比(\e[48;2;30;30;46m = #1E1E2E 暗 vs \e[48;2;250;250;250m = #FAFAFA 亮)。所以亮度分支是通过真实主题管线正确接通的,不只是孤立成立。

结论:从第二种抓取方式确认了「反色 → 显式、跟随主题的背景」在真实界面上的切换,且暗/亮翻转端到端为真。同意 LGTM。(无新问题;也同意上面的提示 —— 新增的 vitest 用例在 chalk.level === 0 下看不到颜色、本身挡不住反色回归,在这些测试里强制一个 color level 就能补上这个缺口。)

}

export function renderSoftwareCursor(text: string): string {
return chalk.bgHex(getSoftwareCursorBackground())(text || ' ');

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.

[Critical — two regressions from chalk.inverse]

1. Missing foreground color: chalk.bgHex() sets only the background. On dark themes (the default), the terminal's text foreground is white while the cursor background becomes #D4D4D4 — a contrast ratio of ~1.1:1, making the cursor character invisible. The old chalk.inverse() swapped fg/bg automatically.

2. No truecolor fallback: On terminals without 24-bit color (chalk.level === 0TERM=dumb, some SSH/CI environments), chalk.bgHex() produces zero escape codes and the cursor disappears entirely. The codebase already has supportsTrueColor() in color-utils.ts.

Suggested fix addressing both issues:

Suggested change
return chalk.bgHex(getSoftwareCursorBackground())(text || ' ');
import { supportsTrueColor } from '../themes/color-utils.js';
const DARK_TEXT = '#1E1E1E';
const LIGHT_TEXT = '#F0F0F0';
export function renderSoftwareCursor(text: string): string {
if (!supportsTrueColor()) {
return chalk.inverse(text || ' ');
}
const bg = getSoftwareCursorBackground();
const fg = bg === LIGHT_CURSOR_BACKGROUND ? DARK_TEXT : LIGHT_TEXT;
return chalk.bgHex(bg).hex(fg)(text || ' ');
}

— Claude 3.5 Sonnet via Qwen Code /review

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.

semi-invisible cursor in Alacritty

4 participants