Skip to content

feat(cli): cap inline shell output with configurable line limit#3508

Merged
tanzhenxin merged 3 commits into
QwenLM:mainfrom
wenshao:feat/shell-output-cap
Apr 22, 2026
Merged

feat(cli): cap inline shell output with configurable line limit#3508
tanzhenxin merged 3 commits into
QwenLM:mainfrom
wenshao:feat/shell-output-cap

Conversation

@wenshao

@wenshao wenshao commented Apr 21, 2026

Copy link
Copy Markdown
Collaborator

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 lines indicator (ANSI streaming) or MaxSizedBox overflow 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 prompt Run this shell command: seq 1 30.

Before — 30-row Shell tool box dominates the viewport:

│ ✓  Shell seq 1 30 [in /tmp] (输出 1 到 30 的序列)
│
│    1
│    2
│    ... [all 30 lines listed] ...
│    30

After (default cap=5):

│ ✓  Shell seq 1 30 [in /tmp] (输出 1 到 30 的序列)
│
│    ... first 26 lines hidden ...
│    27
│    28
│    29
│    30

!seq 1 30 (user-initiated) still shows all 30 rows — isUserInitiated → forceShowResult bypass works as expected.

Bypass / escape hatches

Trigger Behavior
!-prefix user-initiated command isUserInitiated → forceShowResult → full output
Tool awaiting confirmation forceShowResult=true → full output
True tool failure (timeout / abort / unknown tool / throw) ToolCallStatus.Error → forceShowResult → full output
Embedded PTY shell focused (Ctrl+F) isThisShellFocused=true → full output; release re-collapses
User opt-out ui.shellOutputMaxLines: 0 (workspace .qwen/settings.json or user ~/.qwen/settings.json) → no cap
Custom value e.g. ui.shellOutputMaxLines: 15 → cap at 15

Note: A shell command exiting with non-zero status (e.g. seq 1 30 && false, command not found) does not trigger the Error bypass — the tool itself succeeded, the spawned command failed. This is intentional: cap behavior stays consistent regardless of command exit code, so users don't get an unexpected screen-full of output just because a command happened to fail.

Implementation

packages/cli/src/config/settingsSchema.ts — adds ui.shellOutputMaxLines (number, default 5, showInDialog: true). The SettingsDialog renders it automatically via the existing type: 'number' path; Number.isNaN guards garbage input.

packages/cli/src/ui/components/messages/ToolMessage.tsx — computes shellCapHeight = min(availableHeight, cap) once at the top of ToolMessage (gated on tool name matching Shell / Shell Command). Threaded into <AnsiOutputText availableTerminalHeight=…>, <ShellStatsBar displayHeight=…>, and <StringResultRenderer availableHeight=…> so both the streaming ANSI display and the completed string display (shell.ts emits the final result as a plain string via returnDisplayMessage = result.output) are capped.

const isShellTool = name === SHELL_COMMAND_NAME || name === SHELL_NAME;
const shellCapHeight =
  isShellTool &&
  shellOutputMaxLines > 0 &&
  !forceShowResult &&
  !isThisShellFocused
    ? Math.min(availableHeight ?? shellOutputMaxLines, shellOutputMaxLines)
    : availableHeight;

For non-shell tools shellCapHeight === availableHeight, so other tools' string renderers are unaffected.

Scope notes

  • Only Shell / Shell Command tools are capped. Read, Grep, Write, etc. are untouched.
  • MIN_LINES_SHOWN + 1 = 3 floor still applies to availableHeight; the cap never expands beyond what fits (Math.min semantics).
  • Subagent rendering (AgentExecutionDisplay, ToolCallsList) does not go through ToolMessage's shell branch and is unaffected.
  • +N lines is bounded by the headless xterm buffer height (~30 rows) — a pre-existing limitation of buffer-derived totalLines, not introduced here.
  • Initial commit only caught the streaming ANSI path; second commit (fix(cli): apply shell output cap to completed string display too) caught the completed string path after tmux verification revealed the gap.

Testing

  • 6 new ToolMessage tests cover: ANSI cap default, ANSI bypass via forceShowResult, settings disable, custom cap value, shell completed string capped, non-shell ANSI/string not capped.
  • Existing MockAnsiOutputText / MockShellStatsBar mocks were extended to print availableTerminalHeight / displayHeight for prop-level assertions.
  • Wider sweep: 4282 tests passing (CLI + config), 0 failures, typecheck and ESLint clean.

Manual tmux verification (all completed)

Scenario Result
AI shell seq 1 30 (default cap=5) ... first 26 lines hidden ... + lines 27–30 visible
!seq 1 30 (user-initiated bypass) ✅ all 30 lines visible
Shell exit≠0 (seq 1 30 && false, command not found) ✅ cap still applies (status=Success — by design, see note above)
Ctrl+F focus during long command (Focused) indicator appears + full output expands; release re-collapses
Workspace .qwen/settings.json with shellOutputMaxLines: 0 ✅ cap fully disabled, all 30 lines visible
/settings dialog ✅ "Shell Output Max Lines 5" appears under UI category
ANSI streaming with +N lines indicator (for i in $(seq 1 30); do echo line $i; sleep 0.6; done) ✅ shows last 5 + +4 lines timeout 2m 0.1 KB while streaming
Non-Shell tool string output (covered by unit tests) availableHeight=94 passed unchanged

Test plan (for reviewer reproducibility)

  • AI shell seq 1 30 — confirm last 4 lines + ... first 26 lines hidden ... overflow indicator
  • !seq 1 30 — confirm all 30 lines shown (user-initiated bypass)
  • During for i in $(seq 1 30); do echo line $i; sleep 0.6; done press Ctrl+F — confirm (Focused) and full output; release — confirm cap re-applies
  • Workspace .qwen/settings.json with {"ui": {"shellOutputMaxLines": 0}} — confirm cap disabled
  • /settings dialog — confirm "Shell Output Max Lines" row visible and editable

wenshao added 2 commits April 22, 2026 07:09
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 tanzhenxin 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.

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.

wenshao added a commit to wenshao/codeagents that referenced this pull request Apr 22, 2026
[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
@wenshao

wenshao commented Apr 22, 2026

Copy link
Copy Markdown
Collaborator Author

Both addressed in 4bd3579 — thanks for catching these.

Off-by-one (StringResultRenderer): Confirmed the asymmetry was unintentional — the PR description's 4-line "After" example was the bug, not the spec. Fixed by passing shellCapHeight + 1 to the string path so both render N visible content rows (ANSI: 5 + stats bar; string: 5 + overflow banner). tmux re-verified: completed seq 1 30 now reports ... first 25 lines hidden ... followed by lines 26–30, matching the streaming ANSI path.

Setting validation: Schema-level minimum/integer aren't supported by the current settings infrastructure (no other number setting uses them — maxSessionTurns accepts -1, etc.), so I went with the use-site guard Math.max(0, Math.floor(value || 0)):

  • negatives → 0 → cap disabled (matches the documented opt-out)
  • fractions → floor → whole-row cap
  • non-numeric (raw settings.json edits) → 0 → cap disabled

Added a parameterized test covering -1, 1.5, and a non-numeric value.

@tanzhenxin tanzhenxin 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.

Both follow-ups look good:

  • shellStringCapHeight = shellCapHeight + 1 symmetrizes 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 documented 0 = disabled path, 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.

@tanzhenxin tanzhenxin merged commit d71f2fa into QwenLM:main Apr 22, 2026
12 checks passed
TaimoorSiddiquiOfficial pushed a commit to TaimoorSiddiquiOfficial/HopCode that referenced this pull request Apr 23, 2026
…LM#3508)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
chiga0 pushed a commit that referenced this pull request Apr 24, 2026
* 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
mabry1985 added a commit to protoLabsAI/protoCLI that referenced this pull request May 3, 2026
…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>
xaelistic pushed a commit to xaelistic/qwen-code that referenced this pull request Jun 7, 2026
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type/feature-request New feature or enhancement request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants