feat(cli): cap inline shell output with configurable line limit#3508
Conversation
Long-running shell commands (npm install, find /, build logs) currently
fill the viewport with the full visible PTY buffer (up to availableHeight,
~24 lines on a typical terminal). The output dominates the screen and
pushes prior context off the top.
This caps inline ANSI shell output to a small window (default 5 lines,
matching Claude Code's ShellProgressMessage). The hidden line count is
already surfaced via the existing `+N lines` indicator in
`ShellStatsBar`, so users still know how much was elided.
The cap applies only when nothing in the existing escape-hatch set is
true:
- `forceShowResult` (errors, !-prefix user-initiated commands,
tools awaiting confirmation, agents pending confirmation)
- `isThisShellFocused` (ctrl+f focus on a running embedded PTY shell)
- `ui.shellOutputMaxLines = 0` (user opt-out)
Also adds a new `ui.shellOutputMaxLines` setting (default 5) so users
can adjust or disable the cap. The SettingsDialog renders it
automatically via the existing `type: 'number'` schema path.
Notes on scope:
- Only the `'ansi'` display branch is capped. `'string'`, `'diff'`,
`'todo'`, `'plan'`, `'task'` renderers are untouched.
- `AnsiOutputDisplay` is only produced by shell tools (`shell.ts`,
`shellCommandProcessor.ts`), so other tool outputs are unaffected.
- The `+N lines` count is bounded by the headless xterm buffer height
(~30 rows) — a pre-existing limitation of the buffer-based stats,
not introduced here.
Tests:
- 4 new ToolMessage tests cover cap default, forceShowResult bypass,
settings disable (cap=0), and custom cap value.
- The existing `MockAnsiOutputText` / `MockShellStatsBar` mocks were
extended to print `availableTerminalHeight` / `displayHeight` so
the cap behavior is asserted at the prop level.
Initial PR caught only the streaming ANSI branch. AI shell tools emit
the final completed result through `shell.ts:returnDisplayMessage =
result.output`, which is a plain string. That string went through
`StringResultRenderer` with the unmodified `availableHeight`, so the
cap was effectively bypassed for the steady-state display the user
actually sees most of the time.
Verified manually in tmux: a `seq 1 30` invocation by the AI now
collapses to "first 26 lines hidden ... 27 28 29 30" instead of
listing all 30 rows. `!`-prefix `seq 1 30` still expands fully via
the existing `isUserInitiated → forceShowResult` bypass.
Changes:
- Detect shell tool by name (matches existing `SHELL_COMMAND_NAME` /
`SHELL_NAME` checks already used in this file)
- Rename `ansiAvailableHeight` → `shellCapHeight` since it now
governs the string branch as well
- Pass `shellCapHeight` to `StringResultRenderer`; the value
falls back to `availableHeight` for non-shell tools so other
tools' string output is unaffected
- Two new tests: shell completed string is capped; non-shell
string is not
- Two existing tests updated to use `name="Shell"` so they actually
exercise the cap path (would previously have passed by accident
since the original code didn't check tool name)
Also picks up the auto-regenerated VSCode IDE companion settings
schema entry for `ui.shellOutputMaxLines`.
tanzhenxin
left a comment
There was a problem hiding this comment.
Nice cleanup — the single shellCapHeight threaded through all three sinks reads well, and catching the StringResultRenderer path in a follow-up commit after tmux verification is the right instinct.
A couple of observations, neither blocking:
ANSI vs completed-string off-by-one. MaxSizedBox reserves one row for its overflow banner (visibleContentHeight = targetMaxHeight - 1 at MaxSizedBox.tsx:147-150), so with shellOutputMaxLines: 5 the ANSI path shows 5 content lines but the completed-string path shows 4. The PR description's "After" example actually reflects this (lines 27–30 = 4 visible). Not sure if intentional — if not, passing shellCapHeight + 1 to StringResultRenderer would symmetrize.
Setting validation. The dialog rejects NaN but accepts -1 and 1.5. Negatives silently disable the cap (where only 0 is documented as off), and fractions produce fractional slices / +N.5 lines labels. Cheap to guard: Math.max(0, Math.floor(value ?? default)) at the use site, plus minimum: 0 and integer in the schema.
Feature otherwise looks good.
[PR#3508](QwenLM/qwen-code#3508) —— 'feat(cli): cap inline shell output with configurable line limit',作者 wenshao(即 codeagents 项目维护者本人),2026-04-21 23:10 UTC 提交,OPEN 状态。 状态变更: - item-46 主矩阵:🟡 部分实现 → 🟡 拆分实现中(PR#3155 ✓ `+N lines` + PR#3508 🟡 OPEN 5 行窗口,合并后 ✓ 完整) - 追踪 PR 列:从 PR#3155 单独 → PR#3155 + PR#3508 组合 PR#3508 设计亮点(超越 Claude Code 原设计): 1. 可配置 `ui.shellOutputMaxLines`(默认 5 匹配 Claude,settings dialog 可视化编辑) 2. 6 种 bypass 机制(Claude 仅 verbose mode 1 种):! 用户命令 / 确认等待 / 真实失败 / Ctrl+F focus / opt-out / 自定义值 3. Streaming + 完成态字符串渲染器两处都裁剪 4. 语义区分:exit≠0 不触发 Error bypass——tool success ≠ command exit code(Claude 原设计未体现) 闭环链路(反馈循环最快记录): 1. 2026-04-20 深夜 —— 添加 item-46/47 + Bash Deep-Dive 2. 2026-04-21 09:00 —— 标记 🟡 部分实现 3. 2026-04-21 17:00 —— 结合 PR#3155 勘误为 🟡 变体实现 4. 2026-04-21 23:10 UTC —— 用户亲自提交 PR#3508 规格补充到 PR 提交约 24 小时,项目规格文档不仅描述现状,还在主动塑造实现方向。 PR#3508 合并后应跟进:item-46 ✓ 完整、Bash Deep-Dive 更新、README 66→67、考虑 6-bypass 模式反向优化 Claude Code 建议。
Addresses two non-blocking review observations on QwenLM#3508. Off-by-one between paths: MaxSizedBox reserves one row for its overflow banner when content exceeds maxHeight (visibleContentHeight = max - 1). The ANSI path pre-slices to N in AnsiOutputText so MaxSizedBox sees exactly N rows and renders all N — plus the separate ShellStatsBar line. The string path passes the raw cap and lets MaxSizedBox handle overflow, so it shows N-1 content rows + the banner. Result with cap=5: ANSI showed 5+stats, string showed 4+banner. Pass shellCapHeight + 1 to StringResultRenderer when capping so both paths render N visible content rows. Verified in tmux: the completed Shell tool box now reports `... first 25 lines hidden ...` followed by lines 26-30 (was 26 + lines 27-30). Setting validation: Schema accepts any number; the dialog only rejects NaN. Negatives silently disabled the cap (only 0 is documented as off) and fractional values produced fractional slice counts. Added Math.max(0, Math.floor(value || 0)) at the use site so: - negatives → 0 → cap disabled (matches the documented opt-out) - fractions → floor → whole-row cap - non-numeric (raw settings.json edits) → 0 → cap disabled Schema-level minimum/integer constraints aren't supported by the current settings infrastructure (no other number setting uses them either), so the guard lives at the use site. Tests: - Updated string-cap test to assert lines 26-30 visible (catches the +1 fix; was lines 27-30 before) - New parameterized test covers -1, 1.5, and a non-numeric value
|
Both addressed in 4bd3579 — thanks for catching these. Off-by-one ( Setting validation: Schema-level
Added a parameterized test covering |
tanzhenxin
left a comment
There was a problem hiding this comment.
Both follow-ups look good:
shellStringCapHeight = shellCapHeight + 1symmetrizes the paths at N visible content rows, with the inline comment explaining why the +1 is intentional. Test updated to assert all 5 lines (26–30) visible.Math.max(0, Math.floor(rawShellCap || 0))handles negatives / fractions / NaN cleanly — negatives fall through to the documented0 = disabledpath, which matches intent. Parameterized test covers all three cases.
The note about schema-level integer constraints not being supported by the current settings infra is fair — worth a follow-up PR to add that capability across all number settings, but not something to gate this on.
…LM#3508) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(cli): cap inline shell output with configurable line limit
Long-running shell commands (npm install, find /, build logs) currently
fill the viewport with the full visible PTY buffer (up to availableHeight,
~24 lines on a typical terminal). The output dominates the screen and
pushes prior context off the top.
This caps inline ANSI shell output to a small window (default 5 lines,
matching Claude Code's ShellProgressMessage). The hidden line count is
already surfaced via the existing `+N lines` indicator in
`ShellStatsBar`, so users still know how much was elided.
The cap applies only when nothing in the existing escape-hatch set is
true:
- `forceShowResult` (errors, !-prefix user-initiated commands,
tools awaiting confirmation, agents pending confirmation)
- `isThisShellFocused` (ctrl+f focus on a running embedded PTY shell)
- `ui.shellOutputMaxLines = 0` (user opt-out)
Also adds a new `ui.shellOutputMaxLines` setting (default 5) so users
can adjust or disable the cap. The SettingsDialog renders it
automatically via the existing `type: 'number'` schema path.
Notes on scope:
- Only the `'ansi'` display branch is capped. `'string'`, `'diff'`,
`'todo'`, `'plan'`, `'task'` renderers are untouched.
- `AnsiOutputDisplay` is only produced by shell tools (`shell.ts`,
`shellCommandProcessor.ts`), so other tool outputs are unaffected.
- The `+N lines` count is bounded by the headless xterm buffer height
(~30 rows) — a pre-existing limitation of the buffer-based stats,
not introduced here.
Tests:
- 4 new ToolMessage tests cover cap default, forceShowResult bypass,
settings disable (cap=0), and custom cap value.
- The existing `MockAnsiOutputText` / `MockShellStatsBar` mocks were
extended to print `availableTerminalHeight` / `displayHeight` so
the cap behavior is asserted at the prop level.
* fix(cli): apply shell output cap to completed string display too
Initial PR caught only the streaming ANSI branch. AI shell tools emit
the final completed result through `shell.ts:returnDisplayMessage =
result.output`, which is a plain string. That string went through
`StringResultRenderer` with the unmodified `availableHeight`, so the
cap was effectively bypassed for the steady-state display the user
actually sees most of the time.
Verified manually in tmux: a `seq 1 30` invocation by the AI now
collapses to "first 26 lines hidden ... 27 28 29 30" instead of
listing all 30 rows. `!`-prefix `seq 1 30` still expands fully via
the existing `isUserInitiated → forceShowResult` bypass.
Changes:
- Detect shell tool by name (matches existing `SHELL_COMMAND_NAME` /
`SHELL_NAME` checks already used in this file)
- Rename `ansiAvailableHeight` → `shellCapHeight` since it now
governs the string branch as well
- Pass `shellCapHeight` to `StringResultRenderer`; the value
falls back to `availableHeight` for non-shell tools so other
tools' string output is unaffected
- Two new tests: shell completed string is capped; non-shell
string is not
- Two existing tests updated to use `name="Shell"` so they actually
exercise the cap path (would previously have passed by accident
since the original code didn't check tool name)
Also picks up the auto-regenerated VSCode IDE companion settings
schema entry for `ui.shellOutputMaxLines`.
* fix(cli): symmetrize ANSI/string row counts and clamp shell cap input
Addresses two non-blocking review observations on #3508.
Off-by-one between paths:
MaxSizedBox reserves one row for its overflow banner when content
exceeds maxHeight (visibleContentHeight = max - 1). The ANSI path
pre-slices to N in AnsiOutputText so MaxSizedBox sees exactly N
rows and renders all N — plus the separate ShellStatsBar line.
The string path passes the raw cap and lets MaxSizedBox handle
overflow, so it shows N-1 content rows + the banner.
Result with cap=5: ANSI showed 5+stats, string showed 4+banner.
Pass shellCapHeight + 1 to StringResultRenderer when capping so
both paths render N visible content rows. Verified in tmux: the
completed Shell tool box now reports `... first 25 lines hidden ...`
followed by lines 26-30 (was 26 + lines 27-30).
Setting validation:
Schema accepts any number; the dialog only rejects NaN. Negatives
silently disabled the cap (only 0 is documented as off) and
fractional values produced fractional slice counts. Added
Math.max(0, Math.floor(value || 0)) at the use site so:
- negatives → 0 → cap disabled (matches the documented opt-out)
- fractions → floor → whole-row cap
- non-numeric (raw settings.json edits) → 0 → cap disabled
Schema-level minimum/integer constraints aren't supported by the
current settings infrastructure (no other number setting uses
them either), so the guard lives at the use site.
Tests:
- Updated string-cap test to assert lines 26-30 visible (catches
the +1 fix; was lines 27-30 before)
- New parameterized test covers -1, 1.5, and a non-numeric value
…LM#3508) (#216) Long-running shell tool calls (`npm install`, `find /`, build logs) were rendering the full visible PTY buffer inline (~24 lines on a typical terminal). The output dominated the viewport and pushed prior context off the top. This caps inline shell output to a small window (default 5 lines, matching Claude Code's `ShellProgressMessage`). The hidden line count is surfaced via the `+N lines` ShellStatsBar indicator (ANSI streaming) or the existing MaxSizedBox overflow indicator (completed string). The focused embedded shell bypasses the cap so the user sees the full live transcript while interacting with it. Adapted from QwenLM/qwen-code QwenLM#3508. Adaptations: - Pulled ShellStatsBar into our AnsiOutput.tsx ourselves (the upstream component lived in a QwenLM#3155 prerequisite that we're not porting in full — QwenLM#3155 also brings per-tool elapsed time and OSC 9;4 tab progress bar, which Alacritty doesn't support and we don't want enough to justify the surface). - Slimmed ShellStatsBar to just the `+N lines` count; upstream's `totalBytes` (UTF-8 byte size) display is part of the broader QwenLM#3155 scope. - Compute totalLines from `displayRenderer.data.length` directly rather than via upstream's `effectiveDisplayRenderer.stats` channel (also a QwenLM#3155 thing). - Skipped upstream's `forceShowResult` bypass (no consumer in our fork) — the focused-embedded-shell bypass is enough. New setting: `ui.shellOutputMaxLines` (number, default 5). Set to 0 to disable the cap entirely. Co-authored-by: Automaker <automaker@localhost> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…LM#3508) * feat(cli): cap inline shell output with configurable line limit Long-running shell commands (npm install, find /, build logs) currently fill the viewport with the full visible PTY buffer (up to availableHeight, ~24 lines on a typical terminal). The output dominates the screen and pushes prior context off the top. This caps inline ANSI shell output to a small window (default 5 lines, matching Claude Code's ShellProgressMessage). The hidden line count is already surfaced via the existing `+N lines` indicator in `ShellStatsBar`, so users still know how much was elided. The cap applies only when nothing in the existing escape-hatch set is true: - `forceShowResult` (errors, !-prefix user-initiated commands, tools awaiting confirmation, agents pending confirmation) - `isThisShellFocused` (ctrl+f focus on a running embedded PTY shell) - `ui.shellOutputMaxLines = 0` (user opt-out) Also adds a new `ui.shellOutputMaxLines` setting (default 5) so users can adjust or disable the cap. The SettingsDialog renders it automatically via the existing `type: 'number'` schema path. Notes on scope: - Only the `'ansi'` display branch is capped. `'string'`, `'diff'`, `'todo'`, `'plan'`, `'task'` renderers are untouched. - `AnsiOutputDisplay` is only produced by shell tools (`shell.ts`, `shellCommandProcessor.ts`), so other tool outputs are unaffected. - The `+N lines` count is bounded by the headless xterm buffer height (~30 rows) — a pre-existing limitation of the buffer-based stats, not introduced here. Tests: - 4 new ToolMessage tests cover cap default, forceShowResult bypass, settings disable (cap=0), and custom cap value. - The existing `MockAnsiOutputText` / `MockShellStatsBar` mocks were extended to print `availableTerminalHeight` / `displayHeight` so the cap behavior is asserted at the prop level. * fix(cli): apply shell output cap to completed string display too Initial PR caught only the streaming ANSI branch. AI shell tools emit the final completed result through `shell.ts:returnDisplayMessage = result.output`, which is a plain string. That string went through `StringResultRenderer` with the unmodified `availableHeight`, so the cap was effectively bypassed for the steady-state display the user actually sees most of the time. Verified manually in tmux: a `seq 1 30` invocation by the AI now collapses to "first 26 lines hidden ... 27 28 29 30" instead of listing all 30 rows. `!`-prefix `seq 1 30` still expands fully via the existing `isUserInitiated → forceShowResult` bypass. Changes: - Detect shell tool by name (matches existing `SHELL_COMMAND_NAME` / `SHELL_NAME` checks already used in this file) - Rename `ansiAvailableHeight` → `shellCapHeight` since it now governs the string branch as well - Pass `shellCapHeight` to `StringResultRenderer`; the value falls back to `availableHeight` for non-shell tools so other tools' string output is unaffected - Two new tests: shell completed string is capped; non-shell string is not - Two existing tests updated to use `name="Shell"` so they actually exercise the cap path (would previously have passed by accident since the original code didn't check tool name) Also picks up the auto-regenerated VSCode IDE companion settings schema entry for `ui.shellOutputMaxLines`. * fix(cli): symmetrize ANSI/string row counts and clamp shell cap input Addresses two non-blocking review observations on QwenLM#3508. Off-by-one between paths: MaxSizedBox reserves one row for its overflow banner when content exceeds maxHeight (visibleContentHeight = max - 1). The ANSI path pre-slices to N in AnsiOutputText so MaxSizedBox sees exactly N rows and renders all N — plus the separate ShellStatsBar line. The string path passes the raw cap and lets MaxSizedBox handle overflow, so it shows N-1 content rows + the banner. Result with cap=5: ANSI showed 5+stats, string showed 4+banner. Pass shellCapHeight + 1 to StringResultRenderer when capping so both paths render N visible content rows. Verified in tmux: the completed Shell tool box now reports `... first 25 lines hidden ...` followed by lines 26-30 (was 26 + lines 27-30). Setting validation: Schema accepts any number; the dialog only rejects NaN. Negatives silently disabled the cap (only 0 is documented as off) and fractional values produced fractional slice counts. Added Math.max(0, Math.floor(value || 0)) at the use site so: - negatives → 0 → cap disabled (matches the documented opt-out) - fractions → floor → whole-row cap - non-numeric (raw settings.json edits) → 0 → cap disabled Schema-level minimum/integer constraints aren't supported by the current settings infrastructure (no other number setting uses them either), so the guard lives at the use site. Tests: - Updated string-cap test to assert lines 26-30 visible (catches the +1 fix; was lines 27-30 before) - New parameterized test covers -1, 1.5, and a non-numeric value
Summary
Long-running shell tool calls (
npm install,find /, build logs) currently render the full visible PTY buffer inline (~24 lines on a typical terminal). The output dominates the viewport and pushes prior context off the top.This PR caps inline shell output (both streaming ANSI and the completed string display) to a small window (default 5 lines, matching Claude Code's
ShellProgressMessage). The hidden line count is surfaced via the existing+N linesindicator (ANSI streaming) orMaxSizedBoxoverflow indicator (completed string).UX before/after — verified in tmux
Captured with
tmux new-session -d -x 120 -y 40 'node dist/cli.js --yolo', AI promptRun this shell command: seq 1 30.Before — 30-row Shell tool box dominates the viewport:
After (default cap=5):
!seq 1 30(user-initiated) still shows all 30 rows —isUserInitiated → forceShowResultbypass works as expected.Bypass / escape hatches
!-prefix user-initiated commandisUserInitiated → forceShowResult→ full outputforceShowResult=true→ full outputToolCallStatus.Error → forceShowResult→ full outputisThisShellFocused=true→ full output; release re-collapsesui.shellOutputMaxLines: 0(workspace.qwen/settings.jsonor user~/.qwen/settings.json) → no capui.shellOutputMaxLines: 15→ cap at 15Implementation
packages/cli/src/config/settingsSchema.ts— addsui.shellOutputMaxLines(number, default 5,showInDialog: true). The SettingsDialog renders it automatically via the existingtype: 'number'path;Number.isNaNguards garbage input.packages/cli/src/ui/components/messages/ToolMessage.tsx— computesshellCapHeight = min(availableHeight, cap)once at the top ofToolMessage(gated on tool name matchingShell/Shell Command). Threaded into<AnsiOutputText availableTerminalHeight=…>,<ShellStatsBar displayHeight=…>, and<StringResultRenderer availableHeight=…>so both the streaming ANSI display and the completed string display (shell.tsemits the final result as a plain string viareturnDisplayMessage = result.output) are capped.For non-shell tools
shellCapHeight === availableHeight, so other tools' string renderers are unaffected.Scope notes
Shell/Shell Commandtools are capped.Read,Grep,Write, etc. are untouched.MIN_LINES_SHOWN + 1 = 3floor still applies toavailableHeight; the cap never expands beyond what fits (Math.minsemantics).AgentExecutionDisplay,ToolCallsList) does not go throughToolMessage's shell branch and is unaffected.+N linesis bounded by the headless xterm buffer height (~30 rows) — a pre-existing limitation of buffer-derivedtotalLines, not introduced here.fix(cli): apply shell output cap to completed string display too) caught the completed string path after tmux verification revealed the gap.Testing
ToolMessagetests cover: ANSI cap default, ANSI bypass viaforceShowResult, settings disable, custom cap value, shell completed string capped, non-shell ANSI/string not capped.MockAnsiOutputText/MockShellStatsBarmocks were extended to printavailableTerminalHeight/displayHeightfor prop-level assertions.Manual tmux verification (all completed)
seq 1 30(default cap=5)... first 26 lines hidden ...+ lines 27–30 visible!seq 1 30(user-initiated bypass)seq 1 30 && false,command not found)(Focused)indicator appears + full output expands; release re-collapses.qwen/settings.jsonwithshellOutputMaxLines: 0/settingsdialog+N linesindicator (for i in $(seq 1 30); do echo line $i; sleep 0.6; done)+4 lines timeout 2m 0.1 KBwhile streamingavailableHeight=94passed unchangedTest plan (for reviewer reproducibility)
seq 1 30— confirm last 4 lines +... first 26 lines hidden ...overflow indicator!seq 1 30— confirm all 30 lines shown (user-initiated bypass)for i in $(seq 1 30); do echo line $i; sleep 0.6; donepress Ctrl+F — confirm(Focused)and full output; release — confirm cap re-applies.qwen/settings.jsonwith{"ui": {"shellOutputMaxLines": 0}}— confirm cap disabled/settingsdialog — confirm "Shell Output Max Lines" row visible and editable