feat(cli): select an output style via general.outputStyle or --output-style - #10283
feat(cli): select an output style via general.outputStyle or --output-style#10283qqqys wants to merge 8 commits into
Conversation
…-style Adds the first selection surface for the output styles QwenLM#9565 shipped: a `general.outputStyle` setting and a per-run `--output-style` flag that overrides it. Names resolve case-insensitively against the built-in styles; unset, empty, or "default" means no style, and an unknown name prints a warning and starts the session with the default style instead of refusing to start. Claude-Session: https://claude.ai/code/session_01Gk1gryVzWuW58zkBtoBvnM
|
|
|
@qwen-code /takeover |
|
🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the 中文说明🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 |
|
Thanks for the PR! Template looks good ✓ Problem: real and observed, not theoretical — #9565 merged the output-style prompt layer ( Direction: aligned. Output styles are an established, actively maintained feature in the reference product — Claude Code's CHANGELOG records the built-in "Concise" style, then the Size: touches core-protected paths ( Approach: the scope feels right for a first slice. Resolving the name in Risk: no high-risk path matches; no elevated risk signals. Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 问题: 真实且已观测到,不是理论问题 —— #9565 合并了输出风格提示词层( 方向: 对齐。输出风格在参考产品中是成熟且持续维护的功能 —— Claude Code 的 CHANGELOG 记录了内置 "Concise" 风格、随后弃用 规模: 触及核心保护路径( 方案: 作为第一个切片范围合理。在 风险: 无高风险路径命中,无升级风险信号。 进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewReviewed the full diff (145 additions, 0 deletions, 6 files) against the base at the commit below. No blocking findings, no convention violations.
What the suite does not exercise: the end-to-end chain with a live request. Each hop is unit-tested (this PR pins argv/settings → Test evidencePer the unattended-CI rule I did not build or run any PR code; the evidence is the PR's own CI on the reviewed commit, fetched via the API. Final CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 Sandboxed verification would settle the remaining claim: Not verified: live-model behavior of the styles (no e2e run exists in this PR or in this review); the 中文说明代码审查对照下述 commit 的 base 审阅了完整 diff(6 个文件,+145 / -0)。无阻塞性问题,无约定违规。
套件没有覆盖的部分:用真实请求跑端到端链路。每一跳都有单元测试(本 PR 钉住 argv/设置 → 测试证据按无人值守 CI 规则,未构建或运行任何 PR 代码;证据来自 PR 自身在受审 commit 上的 CI,通过 API 获取。承载本改动的单元测试 沙盒验证可以补齐剩余论断: 未验证:风格的真实模型效果(本 PR 与本次评审均无端到端运行); — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 4/5 — clean static review and every claim in the PR body checked out against the base code; the two reservations are that the unit suite was still running on this commit at review time, and the end-to-end prompt-on-the-wire effect rests on the This is a well-cut slice. My independent read of the problem — "the #9565 styles need a selector; resolve it in Nothing to request changes for; no blocking findings. Approving — but the unit suite ( 中文说明Confidence: 4/5 —— 静态审查干净,PR 正文中的每一项论断都已对照 base 代码核实;两点保留意见:评审时该 commit 的单元测试套件仍在运行,且"提示词真正上送"的端到端效果依赖上文指出的 这是一个切分得当的切片。我对问题的独立判断 —— "#9565 的风格需要一个选择器;像其他所有面向 CLI 的选项一样在 没有需要请求修改的事项,也没有阻塞性问题。批准 —— 但评审时单元测试套件( — Qwen Code · qwen3.8-max Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Test Plan (not a blocker): src/config/config.test.ts — no such file or directory.
中文说明
Test Plan(非阻断):src/config/config.test.ts — no such file or directory。
— qwen3.8-max via Qwen Code /review (v0.22.2)
…ngs schema Review findings on QwenLM#10283: - R1-1: `resolveOutputStyle` called `.trim()` on an unvalidated value, so a hand-edited `"general": { "outputStyle": 1 }` — or a repeated `--output-style`, which yargs delivers as an array despite `type: 'string'` — threw a TypeError inside `loadCliConfig` and killed every launch. Both inputs are now `unknown` and a non-string value warns and falls back to the default style, the same no-lockout path an unknown name already took. Picking the source now also survives a null argv. - R1-2: regenerate `settings.schema.json`, which the new `SETTINGS_SCHEMA` entry never reached. This is what reddened "Check settings schema is up-to-date" in the Test job. - R1-3: `general.outputStyle` is read once at startup and frozen into `Config` — nothing applies a mid-session change — so `requiresRestart` is now `true`, matching `general.outputLanguage`. - R1-4: the `default` sentinel and the empty-value early return are now pinned by tests that also assert no warning was printed. - R1-5/R1-6/R1-7: add `--output-style` to both reference tables that list its `--system-prompt` siblings, and document that a style is dropped when `--system-prompt` or `QWEN_SYSTEM_MD` replaces the built-in prompt. Mutation-verified: dropping the typeof guard reddens 5 tests with `raw.trim is not a function`; dropping the `default` sentinel reddens 2; dropping `.trim()` reddens 1; flipping `requiresRestart` back reddens the schema test. Claude-Session: https://claude.ai/code/session_01VXsC4f71S6U6YkW82NRw7m
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Test Plan (not a blocker): 418 tests pass — this review observed 25176, 605 passed.
中文说明
Test Plan(非阻断):418 tests pass — this review observed 25176, 605 passed。
— qwen3.8-max via Qwen Code /review (v0.22.2)
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Autofix round 3 — no action neededNo code changes were required this round. All seven round-1 findings were re-verified by execution against the current head Finding verification (all fixed in
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Test Plan (not a blocker): 418 tests pass — this review observed 25528, 605 passed.
Deferred under the convergence posture (round 3, not a blocker) — recorded, not requested in this round:
packages/cli/src/config/config.ts:1528 — [probe] No test pins the raw === null early-return branch of resolveOutputStylepackages/cli/src/config/config.ts:1556 — [probe] Style warning prints once per loadCliConfig call (sandbox host+child / ACP re-run it); TLS precedent self-dedupes
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
Test Plan(非阻断):418 tests pass — this review observed 25528, 605 passed。
收敛姿态下延后(第 3 轮,非阻断)——已记录,本轮不要求修改:共 2 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| const known = BUILT_IN_OUTPUT_STYLES.map((s) => s.name).join(', '); | ||
| warnAboutOutputStyle( | ||
| `Unknown output style "${name}" (from ${source}); using the default style. Available styles: ${known}.`, | ||
| ); |
There was a problem hiding this comment.
[Critical] R3-1: The unknown-style warning interpolates the raw, unsanitized general.outputStyle value into terminal output — a repo-committed workspace .qwen/settings.json can thereby inject ANSI/OSC control sequences into the terminal of any user who trusts the folder and starts the CLI, bypassing the codebase's own centralized sanitizer (stripAnsiAndControl, whose doc says it is for "ANY untrusted string that may reach a terminal — … values interpolated into error messages").
An attacker commits {"general": {"outputStyle": "<ESC>[2J<ESC>[H✅ Trusted. No issues found.<ESC>]0;pwned<BEL>"}}; a victim clones the repo and trusts the folder (required for any work there — mergeSettings merges workspace settings on trust, and general.outputStyle is not among the WORKSPACE_RESTRICTED_SETTINGS stripped from that scope), then runs qwen. loadCliConfig hits the unknown-name path and console.error emits the raw escape bytes to stderr at startup — before the TUI owns the screen, and directly into the captured stderr stream on qwen -p headless/CI runs. The repo author can thereby paint spoofed startup output over the real session start, hide this very warning, and deliver OSC sequences (window title and worse on terminals that honor them). The same raw string also goes to debugLogger.warn. The non-string invalid-value warning does not echo the value, so this interpolation is the only raw-value echo.
Witness (probe on the unmodified PR code at this commit, scratch tree): calling loadCliConfig with that setting value, the captured stderr reports emittedContainsESC: true, emittedContainsBEL: true, emittedContainsClearScreen: true — the raw clear-screen/cursor-home ESC sequences and the OSC title-set reach the terminal. Applying the fix below flips the probe to all-false, and the 16 pre-existing output-style tests still pass.
Fix (verified in the scratch tree): sanitize before the trim in the string path, so both the console and the debug log get the cleaned value:
const name = stripAnsiAndControl(raw).trim();(import stripAnsiAndControl from @qwen-code/qwen-code-core — already exported, with ~52 existing call sites).
If you add this guard, please pin it with a test: load loadCliConfig with outputStyle: '\u001b[31mEVIL\u001b[0m' and assert the warning captured by the console.error spy contains no \u001b control character (and the style stays unset) — then confirm that test goes red when the sanitize call is removed; the existing 'warns about an unknown style…' test would not catch the regression since 'Verbose' carries no control characters.
中文说明
[Critical] R3-1:未知风格的警告把未经净化的原始 general.outputStyle 值直接插值进终端输出——仓库中提交的 workspace .qwen/settings.json 因此可以向任何信任该文件夹并启动 CLI 的用户的终端注入 ANSI/OSC 控制序列,绕过了代码库自己的集中净化器(stripAnsiAndControl,其文档明确说明它用于"任何可能到达终端的不可信字符串——……插入错误消息的值")。
攻击者提交 {"general": {"outputStyle": "<ESC>[2J<ESC>[H✅ Trusted. No issues found.<ESC>]0;pwned<BEL>"}};受害者克隆仓库并信任该文件夹(在那里做任何工作都必须信任——mergeSettings 在信任时合并 workspace 设置,而 general.outputStyle 不在从该作用域剥离的 WORKSPACE_RESTRICTED_SETTINGS 之列),然后运行 qwen。loadCliConfig 走到未知名称分支,console.error 在启动时把原始转义字节打到 stderr——此时 TUI 尚未接管屏幕,而在 qwen -p headless/CI 运行中则直接进入被捕获的 stderr 流。仓库作者借此可以在真实会话启动之上覆盖伪造的启动输出、隐藏这条警告本身,并投递 OSC 序列(窗口标题,以及在支持的终端上更严重的行为)。同样的原始字符串也会进入 debugLogger.warn。非字符串的非法值警告不会回显该值,因此这处插值是唯一回显原始值的地方。
证据(在本提交的未修改代码上运行探针,临时树):以该设置值调用 loadCliConfig,捕获的 stderr 报告 emittedContainsESC: true, emittedContainsBEL: true, emittedContainsClearScreen: true——原始的清除屏幕/光标归位 ESC 序列与 OSC 标题设置确实到达了终端。应用下面的修复后探针翻转为全 false,且 16 个已有的 output-style 测试仍然通过。
修复(已在临时树中验证):在字符串分支的 trim 之前净化,使 console 与调试日志拿到的都是清理后的值:const name = stripAnsiAndControl(raw).trim();(从 @qwen-code/qwen-code-core 导入 stripAnsiAndControl——已导出,现有约 52 处调用)。
修复见证:如果加上这个守卫,请补一个测试钉住它——以 outputStyle: '\u001b[31mEVIL\u001b[0m' 加载 loadCliConfig,断言 console.error 监听器捕获到的警告中不含 \u001b 控制字符(且风格保持未设置)——然后移除净化调用,确认该测试变红;现有的 'warns about an unknown style…' 测试抓不到这个回归,因为 'Verbose' 不含任何控制字符。
— qwen3.8-max via Qwen Code /review (v0.22.2)
…qwen-code into feat/output-style-setting
|
🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下: Autofix review round summary — PR #10283Commit: Feedback points and dispositions1. [rc:3877667701] Critical R3-1 — unknown-style warning echoes the raw
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed. Suggestions are inline.
1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- R4-3 No test pins the outputStyle null early-return branch — already reported as the round-3 deferral at config.ts:1528 (review 5047654096)
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Test Plan (not a blocker): 418 tests pass — this review observed 25559, 605 passed.
Deferred under the convergence posture (round 4, not a blocker) — recorded, not requested in this round:
packages/cli/src/config/config.ts:1534 — [review] Repeated --output-style discards both names silentlydocs/users/configuration/settings.md:83 — [review] outputStyle docs row omits the restart notepackages/cli/src/config/config.ts:1542 — [probe] Empty --output-style silently overrides the settingpackages/cli/src/config/config.ts:2226 — [probe] outputStyle setting survives --bare and --safe-mode
Convergence: round 4 posted 1 inline comment(s), 1 of them reported for the first time; the previous round posted 1 (1 new). Findings keep coming back to the same files: packages/cli/src/config/config.ts (findings in round 3; 1 more now). The rate of new findings is not falling. A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. Batching the remaining fixes and verifying them before the next push, or dropping this PR's reviews to --severity-floor critical, keeps the loop from re-deriving the same set. No Critical finding is open on this round, so merging and moving the remaining Suggestion threads to a follow-up issue is available as an ending — a merged pull request cannot diverge further. (Observation only — nothing was withheld from this review because of this observation.)
中文说明
仅完成部分审查,审查缺口已披露。 建议见行内评论。
本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
Test Plan(非阻断):418 tests pass — this review observed 25559, 605 passed。
收敛姿态下延后(第 4 轮,非阻断)——已记录,本轮不要求修改:共 4 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 4 轮发布了 1 条行内评论,其中 1 条是首次提出;上一轮发布了 1 条(其中 1 条首次提出)。发现反复回到同一批文件:packages/cli/src/config/config.ts(第 3 轮已出过发现,本轮又有 1 条)。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,或将本 PR 的评审降到 --severity-floor critical,可以避免循环反复推导同一组发现。本轮没有未决的 Critical,因此"合入后把剩余 Suggestion 线程转到后续 issue"是一个可选的结束方式——已合入的 PR 不会继续发散。(仅为观察——本轮评审未因此扣留任何内容。)
— qwen3.8-max via Qwen Code /review (v0.22.2)
| // A repo-committed .qwen/settings.json is untrusted input; strip control | ||
| // sequences so the warning cannot inject terminal escapes from it. | ||
| const name = stripAnsiAndControl(raw).trim(); |
There was a problem hiding this comment.
[Suggestion] R4-4: The sanitize-before-echo guard closes ANSI/OSC/C0-C1 injection exactly as its comment claims, but Unicode bidi and zero-width format characters (U+202A–U+202E, U+2066–U+2069, U+200B–U+200F, U+FEFF) pass through stripAnsiAndControl, so a repo-committed .qwen/settings.json can still visually reorder or spoof this warning line on bidi-aware terminals — the injection class the comment and the new test claim to close. A hostile repo can commit "general": { "outputStyle": "\u202E…reversed text…\u202C" } (or a zero-width suffix such as "Concise\u200b"); anyone who trusts the folder and runs qwen gets the warning rendered with live bidi overrides — masking that the style was rejected or presenting fabricated text — and Unknown output style "Concise\u200b" quotes what looks like the valid name. Impact is display-level only (a hostile checkout already grants stronger primitives), hence not Critical.
Witness (probe on the real built sanitizer at this commit): U+202A, U+202E, U+2066, U+2069, U+200B, U+200F, U+FEFF all survive ('A'+char+'B' keeps length 3 for each), while the control case '\u001b[31mEVIL\u001b[0m\u0007' strips to 'EVIL'; "Concise\u200b" survives strip + trim, fails getBuiltInOutputStyle, and emits the unknown-style warning quoting the look-alike name.
Fix centrally in packages/core/src/utils/textUtils.ts, per the function's own docstring — a bypass fixed there is fixed for every call site:
// eslint-disable-next-line no-control-regex
const CONTROL_CHARS_RE = /[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u202a-\u202e\u2060-\u2069\u206a-\u206f\ufeff]/g;If you add this, please pin it: extend the stripAnsiAndControl tests in packages/core/src/utils/textUtils.test.ts with a case asserting \u202E/\u2067/\u200B are stripped, and strengthen 'strips control sequences before echoing an unknown style name' in config.test.ts to reject zero-width/bidi characters too — removing the added ranges from the regex must turn both red.
中文说明
这处“先净化再回显”的守卫确实如其注释所声明的那样封闭了 ANSI/OSC/C0-C1 注入,但 Unicode 双向控制符与零宽格式字符(U+202A–U+202E、U+2066–U+2069、U+200B–U+200F、U+FEFF)会原样穿过 stripAnsiAndControl,因此仓库中提交的 .qwen/settings.json 仍然可以在支持双向文本的终端上对这条警告做视觉重排或伪造——这正是注释与新增测试声称要封闭的注入类别。攻击者可以提交 "general": { "outputStyle": "\u202E…反转文本…\u202C" }(或零宽后缀,如 "Concise\u200b");任何信任该文件夹并运行 qwen 的用户都会看到带着活跃双向控制符的警告——可以掩盖“风格被拒绝”这一事实,或呈现伪造的文本——并且 Unknown output style "Concise\u200b" 引用的是一个看起来合法的名字。影响仅限于显示层面(恶意检出本身已提供更强的攻击原语),因此不是 Critical。
证据(在本提交上对真实构建出的净化器运行探针):U+202A、U+202E、U+2066、U+2069、U+200B、U+200F、U+FEFF 全部存活(对每个字符 'A'+char+'B' 长度保持为 3),而对照 '\u001b[31mEVIL\u001b[0m\u0007' 被净化为 'EVIL';"Concise\u200b" 穿过净化与 trim 后无法匹配 getBuiltInOutputStyle,未知风格警告引用了这个看似合法的名字。
请按该函数自身文档字符串的约定在 packages/core/src/utils/textUtils.ts 中集中修复——在那里修复一处绕过,所有调用点同时得到修复(正则见上方英文代码块)。
如果加上此修复,请补测试钉住它:在 packages/core/src/utils/textUtils.test.ts 的 stripAnsiAndControl 测试中新增断言 \u202E/\u2067/\u200B 被剥离的用例,并加强 config.test.ts 中的 'strips control sequences before echoing an unknown style name' 使其同样拒绝零宽/双向字符——从正则中移除新增范围后两个测试都应变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
|
🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run. 中文说明🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
4 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- repeated per-loadCliConfig style warning — already recorded as the round-3 deferral at config.ts:1556 (review 5047654096)
- no test pins the outputStyle null early-return branch — already recorded as the round-3 deferral at config.ts:1528 (review 5047654096)
- outputStyle setting survives --bare and --safe-mode — already recorded as the round-4 deferral at config.ts:2226 (review 5048388632)
- outputStyle docs row omits the restart note — already recorded as the round-4 deferral at settings.md:83 (review 5048388632)
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Test Plan (not a blocker): 418 tests pass — this review observed 23479, 605 passed.
Deferred under the convergence posture (round 5, not a blocker) — recorded, not requested in this round:
packages/cli/src/config/config.ts:1552 — [review] Unknown-style warning echoes the untrusted settings value with no length boundpackages/cli/src/config/config.ts:1534 — [review] Invalid-value warning names neither the received shape nor the duplicated flag
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 4 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
Test Plan(非阻断):418 tests pass — this review observed 23479, 605 passed。
收敛姿态下延后(第 5 轮,非阻断)——已记录,本轮不要求修改:共 2 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.22.2)
|
🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run. 中文说明🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
3 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- R6-1 outputStyle docs row omits the restart note — already recorded as the round-4 deferral at settings.md:83 (review 5048388632)
- R6-3 No test pins the outputStyle null early-return branch — already recorded as the round-3 deferral at config.ts:1528 (review 5047654096)
- R6-4 Empty --output-style silently overrides the setting — already recorded as the round-4 deferral at config.ts:1542 (review 5048388632)
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Test Plan (not a blocker): 418 tests pass — this review observed 25727, 605 passed.
Deferred under the convergence posture (round 6, not a blocker) — recorded, not requested in this round:
packages/cli/src/config/config.ts:1533 — [probe] Repeated --output-style with identical values is warned as invalid and the requested style is dropped
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 3 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
Test Plan(非阻断):418 tests pass — this review observed 25727, 605 passed。
收敛姿态下延后(第 6 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.22.2)
What this PR does
This adds the first way to actually pick one of the output styles that #9565 shipped: a
general.outputStylesetting and a per-run--output-style <name>flag that overrides it. Names resolve case-insensitively against the built-in styles —Concise,Proactive,Explanatory,Learning— so"outputStyle": "concise"insettings.jsonand--output-style Conciseon the command line select the same thing. An unset or empty value, or the literaldefault, means no style;--output-style defaultis therefore also the way to switch a style off for one run when the setting names one.An unknown name is reported, not fatal. The session prints
WARNING: Unknown output style "Verbose" (from general.outputStyle); using the default style. Available styles: Concise, Proactive, Explanatory, Learning.and starts with the default style, so a typo insettings.jsonnever locks anyone out of the CLI. The warning names whichever source supplied the bad value, and the same text goes to the debug log so it is still discoverable after the terminal scrollback is gone — the same treatment the TLS-verification warning already gets inloadCliConfig.The setting is declared
showInDialog: falsefor now: the/settingspicker and a/output-stylecommand that applies a change mid-session come in the next slice, where the liverefreshSystemInstruction()hand-off can be reviewed on its own. On this PR a style is chosen once at startup and holds for the session, which is exactly what the headless-ppath needs. The settings reference and the headless guide document the setting, the flag, and the fact thatLearning— which hands the user a piece of code and waits — is skipped in headless runs, asgetCoreSystemPromptalready arranges.Why it's needed
#9565 landed the styles and their prompt layering with no way to select one, so today they are unreachable: the only levers a user has for the agent's voice are still
--append-system-prompt(per-invocation, undiscoverable) andQWEN_SYSTEM_MD(an all-or-nothing prompt replacement they must maintain by hand). A settings key gives the choice persistence and a scope (user or project); the flag gives scripts and one-off runs a way to override it without touching a file. Together they make the built-in styles usable end to end for headless and interactive sessions alike, and they are the substrate the interactive picker, custom style files, and theoutput_stylefield onsystem/initwill all read from.Resolving the name in
loadCliConfig— rather than insideConfig— keepsConfigunaware of settings files and argv, which is how every other CLI-facing option (--append-system-prompt,general.preventSystemSleep) already reaches it.Reviewer Test Plan
How to verify
From
packages/cli, runnpx vitest run packages/cli/src/config/config.test.ts packages/cli/src/config/settingsSchema.test.tsfrom the repo root (or the same paths relative topackages/cli) — 418 tests pass, 13 of them new. Theoutput styleblock asserts: no style by default;general.outputStyle: "concise"resolves to the built-inConcise;--output-style Explanatorybeats a setting ofConcise;--output-style defaultyields no style even when the setting names one, and prints nothing; a whitespace-only value does the same; an unknown name leaves the style unset and prints a warning that names the source (general.outputStylevs--output-style) and lists the four available styles; a non-string setting value (number, boolean, object, array) and a repeated--output-styleflag both warn and start with the default style instead of throwing.settingsSchema.test.tspinsrequiresRestart: true.parseArgumentshas a case for the new flag.Mutation-verified, each run against the current tree: dropping the
typeof raw !== 'string'guard reddens 5 tests withraw.trim is not a function; dropping|| name.toLowerCase() === 'default'reddens 2 on the "no warning" assertion; dropping.trim()reddens the whitespace test; flippingrequiresRestartback tofalsereddens the schema test.To see it end to end (not run here — unit tests only):
qwen -p "Explain what this repo does" --output-style Conciseanswers without preamble;qwen -p "..." --output-style Verboseprints the warning on stderr and then answers in the default voice;QWEN_WRITE_SYSTEM_MD=1 qwen -p hi --output-style Concisestill writes a base prompt with no style section, as #9565 specified.npm run typecheck --workspace @qwen-code/qwen-codeis clean for the changed files (the fourCliArgsliterals ingemini.test.tsxgained the new field, which is what makes it required-but-undefined there).eslintandprettier --checkwere run over every changed file;settingsSchema.test.ts,settings.test.ts,config-command.test.ts(258 tests) andgemini.test.tsx(76 tests) pass.Evidence (Before & After)
N/A for the TUI — the setting is not yet shown in
/settings. Before: no--output-styleflag exists andgeneral.outputStyleis ignored. After:qwen --helplists--output-style, and the flag or setting selects a built-in style for the session.Tested on
Environment (optional)
Linux, Node 22, unit tests.
Risk & Scope
settings.jsonshould not stop every contributor's CLI from starting — but it does mean a misspelled flag is easy to miss in a script that discards stderr./settingsentry or/output-stylecommand yet, so a style cannot be changed mid-session; custom.qwen/output-styles/*.mdfiles, extension-provided styles, the per-turn reminder (feat(core): remind the model of the active output style every turn #10282), and theoutput_stylefield onsystem/initare separate slices. The style prompts themselves have not been evaluated against a live model; the end-to-end commands above are what a reviewer can run, not something exercised here beyond unit tests.CliArgsgains a requiredoutputStylefield, which affects only code that builds the whole struct by hand (the four test literals updated here).Linked Issues
Follows #9565; independent of #10282.
中文说明
这个 PR 做了什么
本 PR 提供了第一个真正能选中 #9565 所引入的输出风格的入口:一个
general.outputStyle设置项,以及一个覆盖它的单次运行参数--output-style <name>。名称对内置风格 ——Concise、Proactive、Explanatory、Learning—— 大小写不敏感地解析,所以settings.json里的"outputStyle": "concise"和命令行上的--output-style Concise选中的是同一个东西。未设置、空值或字面量default表示不使用风格;因此当设置项指定了某个风格时,--output-style default也是在单次运行中关闭它的方式。未知的名称只会被报告,不会致命。会话会打印
WARNING: Unknown output style "Verbose" (from general.outputStyle); using the default style. Available styles: Concise, Proactive, Explanatory, Learning.然后以默认风格启动,所以settings.json里的一个拼写错误绝不会把任何人挡在 CLI 之外。警告会指出是哪个来源提供了错误的值,同样的文本也会写入调试日志,这样终端滚动缓冲区消失后仍然可以找到 —— 这与loadCliConfig里 TLS 校验警告已有的处理方式一致。设置项目前声明为
showInDialog: false:/settings中的选择器和能在会话中途切换的/output-style命令会在下一个切片中到来,届时实时的refreshSystemInstruction()交接可以被单独评审。在本 PR 中,风格在启动时选定一次并在整个会话中保持,这正是 headless-p路径所需要的。settings 参考文档和 headless 指南记录了该设置项、参数,以及Learning(它把一段代码交给用户并等待)在 headless 运行中会被跳过这一点 ——getCoreSystemPrompt已经如此安排。为什么需要它
#9565 落地了风格及其提示词分层,却没有任何选择方式,所以今天它们是不可达的:用户能控制 agent 语气的手段仍然只有
--append-system-prompt(每次调用都要带、无从发现)和QWEN_SYSTEM_MD(对提示词的全有或全无式替换,还得自己手工维护)。一个 settings 键让这个选择得以持久化并具有作用域(用户级或项目级);参数则让脚本和一次性运行无需改文件就能覆盖它。二者结合使内置风格在 headless 和交互式会话中都端到端可用,也是交互式选择器、自定义风格文件和system/init上的output_style字段将来共同读取的基础。在
loadCliConfig中解析名称 —— 而不是在Config内部 —— 让Config不必知道 settings 文件和 argv 的存在,这正是其他所有面向 CLI 的选项(--append-system-prompt、general.preventSystemSleep)到达它的方式。评审者测试计划
如何验证
在仓库根目录运行
npx vitest run packages/cli/src/config/config.test.ts packages/cli/src/config/settingsSchema.test.ts(或在packages/cli下使用相对路径)—— 418 个测试通过,其中 13 个是新增的。output style块断言:默认没有风格;general.outputStyle: "concise"解析为内置的Concise;--output-style Explanatory优先于设置项中的Concise;即便设置项指定了风格,--output-style default也得到无风格且不打印任何内容;纯空白值同理;未知名称使风格保持未设置,并打印一条指明来源(general.outputStyle或--output-style)且列出四个可用风格的警告;非字符串的设置值(数字、布尔、对象、数组)以及重复的--output-style参数都会告警并以默认风格启动,而不是抛异常。settingsSchema.test.ts钉住了requiresRestart: true。parseArguments有一个针对新参数的用例。变异验证(均在当前代码上运行):移除
typeof raw !== 'string'守卫会让 5 个测试变红并报raw.trim is not a function;移除|| name.toLowerCase() === 'default'会让 2 个测试在"无警告"断言上变红;移除.trim()会让空白值测试变红;把requiresRestart改回false会让 schema 测试变红。端到端验证(本 PR 未实际运行,仅单元测试):
qwen -p "Explain what this repo does" --output-style Concise会不带铺垫地直接作答;qwen -p "..." --output-style Verbose会在 stderr 打印警告,然后以默认语气作答;QWEN_WRITE_SYSTEM_MD=1 qwen -p hi --output-style Concise写出的仍是不含风格段落的 base 提示词,与 #9565 的规定一致。npm run typecheck --workspace @qwen-code/qwen-code对改动文件无错误(gemini.test.tsx里四处CliArgs字面量补上了新字段,这正是该字段在那里必须存在但为 undefined 的原因)。对每个改动文件运行了eslint和prettier --check;settingsSchema.test.ts、settings.test.ts、config-command.test.ts(258 个测试)以及gemini.test.tsx(76 个测试)全部通过。证据(Before & After)
TUI 方面 N/A —— 该设置项尚未在
/settings中显示。Before:不存在--output-style参数,general.outputStyle被忽略。After:qwen --help列出--output-style,参数或设置项可以为会话选中一个内置风格。测试环境
环境(可选)
Linux,Node 22,单元测试。
风险与影响范围
settings.json里一个过期或拼错的值不应让每位贡献者的 CLI 都无法启动 —— 但这也意味着在丢弃 stderr 的脚本里,拼错的参数容易被忽略。/settings条目或/output-style命令,所以风格不能在会话中途切换;自定义.qwen/output-styles/*.md文件、extension 提供的风格、每轮提醒(feat(core): remind the model of the active output style every turn #10282)以及system/init上的output_style字段是各自独立的切片。风格提示词文案本身尚未在真实模型上评估;上面的端到端命令是供评审者运行的,本 PR 除单元测试外没有实际执行过它们。CliArgs新增一个必填的outputStyle字段,只影响手工构造整个结构体的代码(本 PR 更新了四处测试字面量)。关联 Issue
承接 #9565;与 #10282 相互独立。