Skip to content

fix(cli): prevent selection dialog flicker - #4755

Merged
tanzhenxin merged 14 commits into
QwenLM:mainfrom
ZevGit:codex/fix-selection-flash
Jun 8, 2026
Merged

fix(cli): prevent selection dialog flicker#4755
tanzhenxin merged 14 commits into
QwenLM:mainfrom
ZevGit:codex/fix-selection-flash

Conversation

@ZevGit

@ZevGit ZevGit commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

What this PR does

This PR constrains interactive selection and confirmation dialogs so they stay within the visible terminal frame when height constraints are active.

It also keeps actionable choices visible in small terminals by allowing long prompt or command content to shrink before the option list is clipped. Shell command confirmations now use an explicit hidden-lines indicator when the command preview is truncated, so users are not led to believe that all commands are visible.

Why it's needed

When a tall selection or confirmation dialog appears in a small terminal or tmux pane, the rendered frame can temporarily exceed the terminal height before footer measurement settles. Ink can respond by clearing and repainting the terminal, which shows up as an intermittent flicker.

The fix keeps dialog rendering bounded on the first frame while preserving the existing unconstrained-height mode.

Reviewer Test Plan

How to verify

Open the CLI in a small terminal or tmux pane, then trigger a selection or confirmation prompt with enough body content to exceed the visible rows. The dialog should appear without a full-screen flash, the selection choices should remain visible, and shell command confirmations should show a hidden-lines indicator when the command list is truncated.

Run the targeted regression tests: cd packages/cli && npx vitest run src/ui/layouts/DefaultAppLayout.test.tsx src/ui/layouts/ScreenReaderAppLayout.test.tsx src/ui/components/ShellConfirmationDialog.test.tsx src/ui/components/ConsentPrompt.test.tsx src/ui/components/ApprovalModeDialog.test.tsx src/ui/components/shared/BaseSelectionList.test.tsx

Run repository checks: npm run lint, npm run typecheck, npm run build, and git diff --check.

Evidence (Before & After)

Before: the new layout regression test rendered a 22-line dialog frame in an 8-row terminal scenario, which exceeds the terminal height and can trigger a full terminal repaint.

After: the same scenario is capped to the terminal frame, shell and consent confirmations keep their choices visible under constrained height, truncated shell command previews show a hidden-lines indicator, and the targeted regression test suite passes with 52 passed and 1 skipped.

Tested on

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

Environment (optional)

Tested locally with the package-level Vitest command above plus repository lint, typecheck, build, and diff whitespace checks.

Risk & Scope

  • Main risk or tradeoff: Long dialog body content can be clipped in constrained terminals so the actionable choices remain visible; shell command confirmations make this explicit with a hidden-lines indicator.
  • Not validated / out of scope: Native terminal recordings on Windows and Linux were not captured locally.
  • Breaking changes / migration notes: None.

Linked Issues

N/A

</Box>
</Box>
) : (
<Box flexDirection="column" marginBottom={1} flexShrink={1}>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The constrained branch drops the "Shell Command Execution" title and "A custom command wants to run the following shell commands:" subtitle entirely. Users in small terminals lose context about what subsystem is requesting permission — the dialog shows only a truncated command list and "Do you want to proceed?".

Consider always rendering the title and subtitle above the ternary, and only switching the command list rendering between MaxSizedBox and the unbounded list. Adjust SHELL_CONFIRMATION_FIXED_ROWS to account for the always-visible title rows.

— qwen3.7-max via Qwen Code /review

const { dialogOpen: bgTasksDialogOpen } = useBackgroundTaskViewState();
const { constrainHeight, terminalHeight, staticExtraHeight, mainAreaWidth } =
uiState;
const dialogMaxHeight = Math.max(1, terminalHeight - staticExtraHeight - 2);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The formula Math.max(1, terminalHeight - staticExtraHeight - 2) is independently computed in three places (here, DefaultAppLayout, and ScreenReaderAppLayout). The -2 safety margin is undocumented — future maintainers cannot determine what UI chrome it represents. Additionally, SettingsDialog, StatusLineDialog, and BackgroundTasksDialog receive dialogMaxHeight unconditionally (lines 264, 279, 505), ignoring the constrainHeight flag while all other dialogs use constrainedDialogHeight.

Consider extracting a shared utility with a comment explaining the -2 (e.g. // 1 row for top border + 1 row for bottom border/ExitWarning overlap), and using constrainedDialogHeight consistently for all dialogs or documenting why these three must always be constrained.

— qwen3.7-max via Qwen Code /review

const maxModeItemsToShow =
constrainedHeight === undefined
? DEFAULT_MAX_MODE_ITEMS_TO_SHOW
: Math.max(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] modeListChromeHeight accounts for chrome, spacer, and footer hint rows but omits the showWorkspacePriorityWarning box (~2 rows: marginTop={1} + wrapping <Text>). When the warning is visible in a constrained terminal, the footer keyboard hint or the warning itself may be silently clipped by overflow="hidden".

Add the warning's row count to the budget:

const warningRows = showWorkspacePriorityWarning ? 2 : 0;
const modeListChromeHeight =
  MODE_LIST_CHROME_ROWS +
  (showModeSpacer ? MODE_SPACER_ROWS : 0) +
  (showFooterHint ? FOOTER_HINT_ROWS : 0) +
  warningRows;

— qwen3.7-max via Qwen Code /review

isFeedbackDialogOpen: false,
mainAreaWidth: 80,
terminalWidth: 80,
terminalHeight: 24,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] ScreenReaderAppLayout.tsx receives the same dialogMaxHeight / dialogHeight / overflow changes as DefaultAppLayout.tsx, but no new tests verify the height-constraint behavior. DefaultAppLayout.test.tsx adds two tests (constrained + unconstrained); this file adds only base-UIState fields.

Screen-reader users are the audience most harmed by a layout regression here. The vi.hoisted DialogManager mock pattern from DefaultAppLayout.test.tsx can be reused directly.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

Verification Report

Branch: codex/fix-selection-flash
Base: main
Environment: macOS Darwin 25.4.0, Node.js v22.17.0

Test Results

Check Command Result
DefaultAppLayout Tests vitest run DefaultAppLayout.test.tsx ✅ 7 tests passed
ScreenReaderAppLayout Tests vitest run ScreenReaderAppLayout.test.tsx ✅ 6 tests passed
ShellConfirmationDialog Tests vitest run ShellConfirmationDialog.test.tsx ✅ 5 tests passed
ConsentPrompt Tests vitest run ConsentPrompt.test.tsx ✅ 6 tests passed
ApprovalModeDialog Tests vitest run ApprovalModeDialog.test.tsx ✅ 6 tests passed
BaseSelectionList Tests vitest run BaseSelectionList.test.tsx ✅ 26 passed, 1 skipped
layoutUtils Tests vitest run layoutUtils.test.ts ✅ 3 tests passed
Total 7 test files 58 passed, 1 skipped
ESLint eslint on all 9 changed source files ✅ No errors
CLI Type Check npm run typecheck --workspace=packages/cli ⚠️ 44 errors — none in PR files (main has 3; delta from upstream/main merge)
CLI Build npm run build --workspace=packages/cli ✅ Success
Whitespace git diff --check ✅ Clean

Code Review Notes

  • layoutUtils.ts: New computeDialogMaxHeight() utility correctly computes available dialog height by subtracting fixed chrome (header, footer, borders) from terminal rows. Defaults to Infinity when no constraint is active — preserving existing unconstrained behavior.
  • DefaultAppLayout.tsx / ScreenReaderAppLayout.tsx: Both layouts now pass maxDialogHeight down to DialogManager, computed from useTerminalRows(). New regression tests verify a 22-line dialog is capped to 8 rows when constrained.
  • DialogManager.tsx: Threads maxDialogHeight to ApprovalModeDialog, ConsentPrompt, and ShellConfirmationDialog. Clean prop passthrough.
  • ApprovalModeDialog.tsx: Uses maxDialogHeight to limit maxItemsToShow on BaseSelectionList, ensuring actionable choices stay visible. Prompt text shrinks before option list is clipped. New test verifies 4 options remain visible in an 8-row terminal.
  • ConsentPrompt.tsx: Same pattern — body text is allowed to shrink so consent choices remain visible under height constraints. New test confirms choices visible at 10-row height.
  • ShellConfirmationDialog.tsx: Adds explicit … and N more commands hidden indicator when shell command preview is truncated. New test validates the hidden-lines indicator appears correctly.
  • BaseSelectionList.tsx: Accepts optional maxItemsToShow prop to override the internal default. New test verifies external capping works.
  • doctorCommand.ts: Minor refactor to use the new layout utility.

Verdict

✅ Ready to merge — all 58 tests pass across 7 test files, lint is clean, build succeeds. No typecheck errors in any PR-touched files. The implementation correctly constrains dialog height while preserving unconstrained mode as the default.


Verified by wenshao

marginX={2}
flexDirection="column"
width={uiState.mainAreaWidth}
height={dialogHeight}

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] The outer layout Box clips the bottom of inner dialogs because Ink uses border-box height.

Ink's height includes border rows (verified empirically: a Box with height={4} + borderStyle="round" renders exactly 4 total rows = 2 border + 2 content). This outer Box clips its children at dialogHeight rows, but each inner dialog (e.g. ShellConfirmationDialog with borderStyle="round") occupies dialogHeight + 2 total rows. The bottom 2 rows — where the radio button options render — are silently clipped.

Concrete impact: with staticExtraHeight=3 and DIALOG_VERTICAL_SAFETY_MARGIN=2, the dialog receives constrainedHeight = terminalHeight - 5. At a 15-row terminal (common in split panes and tmux), constrainedHeight = 10: the "No (esc)" option in ShellConfirmationDialog is invisible. At a 13-row terminal, only "Yes, allow once" is visible. The same issue exists in ScreenReaderAppLayout.tsx.

The existing tests don't catch this because ShellConfirmationDialog.test.tsx renders the dialog directly (no outer Box), so the outer clip is never exercised.

Suggested change
height={dialogHeight}
overflow={uiState.constrainHeight ? 'hidden' : undefined}

Remove height={dialogHeight} from the outer Box. Each dialog already receives availableTerminalHeight={constrainedDialogHeight} and self-constrains via its own height + overflow="hidden". The outer Box's height is redundant and causes the double-clipping. Alternatively, if a safety net is desired, reduce the outer height to account for the dialog border: height={dialogHeight !== undefined ? dialogHeight + 2 : undefined}.

— qwen3.7-max via Qwen Code /review

const commandPreviewHeight =
constrainedHeight === undefined
? undefined
: Math.max(2, constrainedHeight - SHELL_CONFIRMATION_FIXED_ROWS);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The Math.max(2, ...) floor forces the command preview to at least 2 rows even when the terminal is too small. This adds 2 rows that compete with the radio buttons for vertical space, causing the options (including "No (esc)") to be clipped at constrainedHeight < 11 (terminal height < 16 rows).

The command preview is informational; the radio buttons are the actionable part and should never be sacrificed for it.

Suggested change
: Math.max(2, constrainedHeight - SHELL_CONFIRMATION_FIXED_ROWS);
: constrainedHeight >= SHELL_CONFIRMATION_FIXED_ROWS + 2
? Math.max(2, constrainedHeight - SHELL_CONFIRMATION_FIXED_ROWS)
: 0;

Then in the JSX, skip MaxSizedBox when commandPreviewHeight is 0:

{commandPreviewHeight ? (
  <Box flexDirection="column" flexShrink={1}>
    <MaxSizedBox maxHeight={commandPreviewHeight} ...>
      ...
    </MaxSizedBox>
  </Box>
) : null}

This ensures the radio buttons always have room when the terminal is small.

— qwen3.7-max via Qwen Code /review

* painting into the terminal's bottom rows before control-height measurement
* settles.
*/
export const getDialogMaxHeight = (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] DIALOG_VERTICAL_SAFETY_MARGIN = 2 is coupled to a bare - 2 literal in AppContainer.tsx (terminalHeight - controlsHeight - staticExtraHeight - 2 - tabBarHeight) only by this JSDoc comment. There is no shared constant or import linking the two values. If someone adds a status bar row or changes staticExtraHeight in AppContainer, the dialog height budget silently becomes wrong — the regression is purely visual and only shows up at specific terminal heights.

Consider extracting the AppContainer-side reservation into a named constant (e.g., MAIN_CONTENT_HEIGHT_RESERVATION) in a shared location and importing it in both AppContainer.tsx and layoutUtils.ts, so the coupling is structural rather than comment-only.

— qwen3.7-max via Qwen Code /review

expect(lastFrame() ?? '').toContain('Automatically approve all tools');
});

it('keeps the workspace priority warning visible when constrained', () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] This test at height 12 verifies both the workspace warning and footer hint are visible. But at heights 10–11 with showWorkspacePriorityWarning = true, showFooterHint becomes false (because constrainedHeight < MIN_HEIGHT_WITH_WARNING_FOOTER_HINT = 12). This footer-hiding branch — which reclaims 2 rows for the mode list — is never exercised.

Consider adding a test:

it('hides the footer hint to make room for the workspace warning', () => {
  const { lastFrame } = renderWithProviders(
    <ApprovalModeDialog
      settings={createSettings({ tools: { approvalMode: ApprovalMode.YOLO } })}
      currentMode={ApprovalMode.DEFAULT}
      availableTerminalHeight={10}
      onSelect={vi.fn()}
    />,
  );
  const frame = lastFrame() ?? '';
  expect(frame).toContain('Workspace approval mode exists');
  expect(frame).not.toContain('Use Enter to select');
});

— qwen3.7-max via Qwen Code /review

@ZevGit

ZevGit commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up after the latest review:

  • Updated ShellConfirmationDialog so very small constrained terminals drop the informational command preview before sacrificing any radio-button actions. Added regression coverage for 10-row and 8-row dialog budgets.
  • Added a regression test that renders the shell confirmation dialog inside a constrained outer layout box. The action rows remain visible in that wrapped case, so I kept the outer layout height cap in place to preserve the original anti-flicker protection instead of removing it.
  • Replaced the comment-only coupling between the dialog height helper and AppContainer with a shared MAIN_CONTENT_HEIGHT_RESERVATION constant.
  • Added ApprovalModeDialog coverage for the workspace-priority warning case where the footer hint is intentionally hidden to reclaim rows.

Verification run locally:

  • cd packages/cli && npx vitest run src/ui/layouts/DefaultAppLayout.test.tsx src/ui/layouts/ScreenReaderAppLayout.test.tsx src/ui/components/ShellConfirmationDialog.test.tsx src/ui/components/ConsentPrompt.test.tsx src/ui/components/ApprovalModeDialog.test.tsx src/ui/components/shared/BaseSelectionList.test.tsx src/ui/utils/layoutUtils.test.ts — 62 passed, 1 skipped
  • npm run lint — passed
  • npm run build — passed
  • npm run typecheck — passed
  • git diff --check — clean

@ZevGit

ZevGit commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

Additional follow-up after rebasing onto the latest main:

  • Merged the current upstream main into this branch so the PR tests against the same base GitHub uses for the merge ref.
  • The previous CI failure reproduced locally after that merge in packages/core/src/tools/skill.ts: the disabled-skill command delegation path treated ModelInvocableCommandExecutorResult as always string-like.
  • Fixed that path to handle { error } command executor results explicitly, mirroring the non-disabled command fallback behavior, and added regression coverage for that case.

Verification run locally after the upstream merge and fix:

  • cd packages/core && npx vitest run src/tools/skill.test.ts — 67 passed
  • npm run typecheck --workspace=packages/core — passed
  • cd packages/cli && npx vitest run src/ui/layouts/DefaultAppLayout.test.tsx src/ui/layouts/ScreenReaderAppLayout.test.tsx src/ui/components/ShellConfirmationDialog.test.tsx src/ui/components/ConsentPrompt.test.tsx src/ui/components/ApprovalModeDialog.test.tsx src/ui/components/shared/BaseSelectionList.test.tsx src/ui/utils/layoutUtils.test.ts — 62 passed, 1 skipped
  • npm run lint — passed
  • npm run build — passed
  • npm run typecheck — passed
  • git diff --check — clean

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] SkillsManagerDialog in DialogManager.tsx:444 still uses the inline formula constrainHeight ? terminalHeight - staticExtraHeight : undefined instead of the centralized constrainedDialogHeight (which subtracts MAIN_CONTENT_HEIGHT_RESERVATION = 2 via getDialogMaxHeight()). Every other height-constrained dialog in this file was migrated — SkillsManagerDialog is the sole holdout, receiving a height budget 2 rows larger than the parent layout Box actually allocates.

          availableTerminalHeight={constrainedDialogHeight}

— qwen3.7-max via Qwen Code /review

Comment thread packages/core/src/tools/skill.ts Outdated
@@ -455,7 +455,13 @@ class SkillToolInvocation extends BaseToolInvocation<SkillParams, ToolResult> {
// makes the system MORE fragile to MCP failures, not less.
try {
const content = await this.commandExecutor(this.params.skill);

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] The disabled-skill fallback calls this.commandExecutor(this.params.skill) without passing this.params.args. The non-disabled path at line 498 passes this.params.args ?? ''. When a disabled skill delegates to an MCP prompt or file command, the model-provided arguments are silently discarded.

Suggested change
const content = await this.commandExecutor(this.params.skill);
const content = await this.commandExecutor(this.params.skill, this.params.args ?? '');

— qwen3.7-max via Qwen Code /review

}

const DEFAULT_MAX_MODE_ITEMS_TO_SHOW = 10;
const CONSTRAINED_DIALOG_HEIGHT_THRESHOLD = 12;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] CONSTRAINED_DIALOG_HEIGHT_THRESHOLD and the derived isVerticallyConstrained flag never change behavior. Tracing all possible constrainedHeight values: for >= 12, isVerticallyConstrained is false so both showModeSpacer and showFooterHint are unconditionally true; for 9–11, the individual threshold checks (>= MIN_HEIGHT_WITH_MODE_SPACER, >= MIN_HEIGHT_WITH_FOOTER_HINT) already return true; for < 9, both branches agree. The flag is dead code that adds cognitive overhead.

Simplify by removing CONSTRAINED_DIALOG_HEIGHT_THRESHOLD and isVerticallyConstrained:

const showModeSpacer =
  constrainedHeight === undefined ||
  constrainedHeight >= MIN_HEIGHT_WITH_MODE_SPACER;
const showFooterHint =
  constrainedHeight === undefined ||
  constrainedHeight >=
    (showWorkspacePriorityWarning
      ? MIN_HEIGHT_WITH_WARNING_FOOTER_HINT
      : MIN_HEIGHT_WITH_FOOTER_HINT);

— qwen3.7-max via Qwen Code /review

constrainedHeight === undefined
? undefined
: constrainedHeight >= SHELL_CONFIRMATION_FIXED_ROWS + 2
? Math.max(2, constrainedHeight - SHELL_CONFIRMATION_FIXED_ROWS)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The height-clamping idiom availableTerminalHeight === undefined ? undefined : Math.max(1, Math.floor(availableTerminalHeight)) is duplicated verbatim across ShellConfirmationDialog, ConsentPrompt, and ApprovalModeDialog. This PR centralizes dialog height constraints via getDialogMaxHeight() in layoutUtils.ts, but each dialog still re-implements this clamping step.

Consider extracting a shared utility:

export const clampDialogHeight = (h: number | undefined): number | undefined =>
  h === undefined ? undefined : Math.max(1, Math.floor(h));

— qwen3.7-max via Qwen Code /review

<MaxSizedBox
maxHeight={commandPreviewHeight}
maxWidth={Math.max(1, contentWidth - 8)}
overflowDirection="top"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] In the constrained path, commands are rendered as plain <Text>{cmd}</Text>, while the unconstrained path uses <RenderInline text={cmd} /> which processes inline markdown. Commands will render differently depending on terminal height — markdown-like characters (backticks, asterisks) in shell commands will be formatted in one view but raw in the other.

Suggested change
overflowDirection="top"
<Text color={theme.text.link}>
<RenderInline text={cmd} />
</Text>

— qwen3.7-max via Qwen Code /review

const promptHeight =
constrainedHeight === undefined
? undefined
: Math.max(1, constrainedHeight - CONSENT_PROMPT_CHROME_ROWS);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] When constrainedHeight <= 8 (small terminals, tmux splits), promptHeight becomes 1 — the consent prompt text is clipped to a single row while the Yes/No RadioButtonSelect (with flexShrink={0}) remains fully visible and interactive. There is no truncation indicator, so users can approve a destructive operation without reading the warning context.

Consider adding a [Content truncated — resize terminal] indicator with flexShrink={0} when promptHeight <= 2, or requiring typed confirmation in very small terminals.

— qwen3.7-max via Qwen Code /review

constrainedHeight === undefined
? undefined
: constrainedHeight >= SHELL_CONFIRMATION_FIXED_ROWS + 2
? Math.max(2, constrainedHeight - SHELL_CONFIRMATION_FIXED_ROWS)

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] When constrainedHeight < SHELL_CONFIRMATION_FIXED_ROWS + 2 (i.e. < 11), commandPreviewHeight is set to 0 and the ternary falls to the null branch — rendering no command list at all. The approve/deny radio options still appear with flexShrink={0}. The user is asked to approve shell commands they literally cannot see.

This triggers at terminal height ~13 with staticExtraHeight=3 (dialogMaxHeight = max(1, 13-3-2) = 8), which is common with tmux splits or small laptop windows. The existing test at line 137 confirms this is by design (expect(frame).not.toContain('cmd-10')).

The shell command confirmation — the most security-critical gate in the CLI — can be bypassed by simply having a small terminal. A user could approve rm -rf $HOME without ever seeing the command.

Consider one of:

  1. When commandPreviewHeight would be 0, drop the title/subtitle (they're already flexShrink-able) to reclaim rows for at least 1-2 lines of command preview.
  2. Replace the null branch with a visible indicator: "(N commands — resize terminal to preview)" so the user knows commands are hidden before approving.
  3. Refuse to show approve options when commands are invisible — require the user to resize or press a key to reveal.

— qwen3.7-max via Qwen Code /review

@ZevGit

ZevGit commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

Updated in 21994e9.

Changes:

  • SkillsManagerDialog now uses the centralized constrained dialog height budget.
  • Disabled-skill command fallback now forwards args consistently with the normal command fallback path.
  • Shell command confirmation no longer silently omits commands in very small terminals: it keeps the dialog title and choices visible and shows a hidden-command notice when the preview cannot fit.
  • Shell commands now render as literal command text in both constrained and unconstrained layouts instead of applying inline markdown in only one path.
  • ConsentPrompt now shows a truncation notice when a constrained prompt is reduced to <=2 review rows.
  • ApprovalModeDialog now uses the shared clamp helper and removes the dead constrained-height threshold branch.

Local verification:

  • packages/cli: npx vitest run src/ui/layouts/DefaultAppLayout.test.tsx src/ui/layouts/ScreenReaderAppLayout.test.tsx src/ui/components/ShellConfirmationDialog.test.tsx src/ui/components/ConsentPrompt.test.tsx src/ui/components/ApprovalModeDialog.test.tsx src/ui/components/shared/BaseSelectionList.test.tsx src/ui/utils/layoutUtils.test.ts
  • packages/core: npx vitest run src/tools/skill.test.ts
  • npm run lint
  • npm run build
  • npm run typecheck
  • git diff --check

GitHub CI has restarted for the new head and is currently pending.

constrainedHeight !== undefined &&
commandPreviewHeight === 0 &&
commands.length > 0;
const compactHiddenCommandsLayout =

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] compactHiddenCommandsLayout activates at constrainedHeight <= SHELL_CONFIRMATION_FIXED_ROWS (9), but the compact layout needs 6 content rows (title 1 + hidden-count message 1 + 4 radio options 4) while the content area is only constrainedHeight - border(2) - paddingY(0) = constrainedHeight - 2 rows. At constrainedHeight = 7: content area = 5, but 6 rows needed → the flexShrink={0} options Box overflows and overflow="hidden" clips the last option ("No (esc)").

The boundary is constrainedHeight >= 8 for the compact layout to fit (content area 6 = needed 6). This is reachable at realistic terminal sizes (e.g. terminalHeight 12 with staticExtraHeight 3 → getDialogMaxHeight(12, 3) = 7).

Suggested change
const compactHiddenCommandsLayout =
const compactHiddenCommandsLayout =
commandsHidden && constrainedHeight <= SHELL_CONFIRMATION_FIXED_ROWS
&& constrainedHeight >= 8;

Or add a further degradation tier that drops the hidden-count message when constrainedHeight < 8, leaving only title + 4 options (5 rows).

— qwen3.7-max via Qwen Code /review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 24431d9. For hidden commands below the 8-row approval threshold, the dialog now keeps the title and hidden-command warning visible but only exposes the cancel option. At 8 rows and above, the compact hidden-command layout can fit the warning plus all approval options. Added a 7-row regression test for this exact clipping case.

onHighlight={handleModeHighlight}
isFocused={mode === 'mode'}
maxItemsToShow={10}
maxItemsToShow={maxModeItemsToShow}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] showScrollArrows is hard-coded to false. Before this PR, maxItemsToShow={10} always fit all 5 modes, so this was harmless. Now that maxModeItemsToShow is dynamic and can be less than modeItems.length at constrained heights, users see a truncated list with no visual indicator that more modes exist below the viewport.

Suggested change
maxItemsToShow={maxModeItemsToShow}
showScrollArrows={maxModeItemsToShow < modeItems.length}

— qwen3.7-max via Qwen Code /review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 24431d9. ApprovalModeDialog now derives showScrollArrows from the constrained mode-list row budget, includes the arrow rows in maxItemsToShow, and hides the non-warning footer hint when needed so the scroll state remains visible. Added coverage for the 8-row truncated list and the 10-row footer tradeoff.

@ZevGit

ZevGit commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

Updated in 24431d9 after the latest review.

Why there were still issues:

  • The previous revision fixed the main hidden-command path at the 8-row dialog budget, but it did not cover the tighter 7-row budget. At that height the hidden-command notice plus all approval options could not fit, so Ink clipped the dialog title and still exposed approval actions.
  • ApprovalModeDialog made the visible mode count dynamic, but scroll arrows were still disabled. That made truncated mode lists look complete in constrained terminals.

Changes in this update:

  • ShellConfirmationDialog now treats hidden commands below the 8-row approval threshold as not approvable: it keeps the title and hidden-command warning visible and only exposes the cancel option until the terminal is resized.
  • ApprovalModeDialog now shows scroll arrows when constrained height hides modes. The arrow rows are included in the list budget, and the non-warning footer hint is hidden when necessary so the scroll state is visible instead of being clipped.

Local verification:

  • packages/cli: npx vitest run src/ui/layouts/DefaultAppLayout.test.tsx src/ui/layouts/ScreenReaderAppLayout.test.tsx src/ui/components/ShellConfirmationDialog.test.tsx src/ui/components/ConsentPrompt.test.tsx src/ui/components/ApprovalModeDialog.test.tsx src/ui/components/shared/BaseSelectionList.test.tsx src/ui/utils/layoutUtils.test.ts — 68 passed, 1 skipped
  • packages/core: npx vitest run src/tools/skill.test.ts — 68 passed
  • npm run lint — passed
  • npm run build — passed
  • npm run typecheck — passed
  • git diff --check — clean

GitHub CI has restarted for the new head.

request={{
commands: Array.from(
{ length: 10 },
(_, i) => `cmd-${String(i + 1).padStart(2, '0')}`,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Boundary heights 9 and 11 are not exercised. Height 9 is the compactHiddenCommandsLayout threshold (subtitle and "Do you want to proceed?" disappear), and height 11 is where commandPreviewHeight transitions from 0 to 2 (commands become visible via MaxSizedBox). An off-by-one in SHELL_CONFIRMATION_FIXED_ROWS or MIN_HEIGHT_WITH_HIDDEN_COMMAND_OPTIONS would go undetected.

Consider adding test cases at these heights:

  • Height 9: assert compact layout (no subtitle, no question text) with all four options still visible
  • Height 11: assert commands are rendered inside MaxSizedBox (command text visible, no "shell commands hidden" notice)

— qwen3.7-max via Qwen Code /review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in ef1ccc5. Added boundary coverage for height 9, where the compact hidden-command layout hides the subtitle/question while keeping all approval options visible, and height 11, where the dialog switches to the MaxSizedBox command preview instead of the hidden-command notice.

availableTerminalHeight={8}
/>,
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The truncation notice threshold is availablePromptRows <= 2 (heights 7–9). This test covers height 8 (availablePromptRows=1) and height 12 (availablePromptRows=5), but the exact boundary is not pinned. Height 9 (availablePromptRows=2, last showing notice) and height 10 (availablePromptRows=3, first without notice) would catch an off-by-one in CONSENT_PROMPT_CHROME_ROWS or the <= 2 condition.

— qwen3.7-max via Qwen Code /review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in ef1ccc5. Added boundary coverage for both sides of the consent prompt truncation threshold: height 9 keeps the truncation notice at the two-row prompt boundary, and height 10 verifies the first three-row prompt case without the notice.

@tanzhenxin tanzhenxin added the type/bug Something isn't working as expected label Jun 8, 2026
@ZevGit

ZevGit commented Jun 8, 2026

Copy link
Copy Markdown
Contributor Author

All requested follow-ups have been addressed on the latest head ef1ccc5fb, including the constrained dialog boundary coverage requested in the last review. GitHub CI is green across lint, CodeQL, and the macOS/Ubuntu/Windows test jobs.

The remaining CHANGES_REQUESTED state appears to be from earlier reviews. Could you please take another look when you have a chance?

@wenshao
wenshao requested a review from tanzhenxin June 8, 2026 06:50

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

Review

The flicker fix is clean and well-targeted — seeding the scroll offset from the active index so a high initial selection no longer renders from the top and jumps — and it's backed by solid boundary-height tests. The shared height-reservation constant is a nice cleanup over the previous duplicated magic number, and the skill.ts change is a welcome bonus: the disabled-skill path was dropping the user's args, and this fixes it.

Approving to unblock this. There's one known limitation worth a follow-up, called out below so it doesn't get lost.

1. Shell commands can be approved while fully hidden in a constrained dialog (severity: medium · confidence: high)

When the dialog is squeezed into roughly 8 to 10 rows, the command preview collapses entirely — the user sees only "N shell commands hidden — resize terminal to review" — but the approve options (Yes, allow once, Always allow…) stay selectable, so a user can approve shell commands they never actually saw. The dialog already does the right thing below 8 rows, where it drops to cancel-only; the gap is that the "commands are hidden" condition keeps holding through 10 rows while the cancel-only restriction only kicks in below 8. It's reachable at non-extreme sizes — a modestly short terminal or a small split pane. Tying the cancel-only behavior to "are the commands hidden" rather than a separate row threshold would close it consistently with the existing below-8 handling. Not blocking the approval, but worth tightening in a follow-up since it's a safety-confirmation path.

Verdict

APPROVE — solid flicker fix and a bonus args correctness fix; one known limitation noted above for a follow-up.

@tanzhenxin
tanzhenxin merged commit fb98d94 into QwenLM:main Jun 8, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type/bug Something isn't working as expected

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants