Skip to content

refactor(TUI): improve command menu scroll and resize mechanics - #2467

Merged
nborges-aws merged 5 commits into
refactorfrom
optimize-small-terminals
Sep 30, 2026
Merged

nborges-aws merged 5 commits into
refactorfrom
optimize-small-terminals

Conversation

@nborges-aws

Copy link
Copy Markdown
Contributor

Description

PR has before and after videos of behavior, make sure to scroll to bottom of description

Improves TUI behavior with optimizations to scroll and resizing mechanics.

Problem:
The command menu had some rough edges with scroll/resize behavior:

  • AgentCore CLI banner took up majority of available space on small terminals
  • Menu items overflowed the available height without scrolling
  • Resizing left duplicate menu items & focus carets on screen
  • Rows wrapping caused line separators to distort, and distortion persisted on all resizing (including enlarging terminal again)
  • Ink repainted the TUI each time the terminal window changed by one pixel, causing flickering when resizing terminal

Before this PR:

Screen.Recording.2026-09-29.at.8.23.01.PM.mov

Changes:

  • Hide the AgentCore CLI banner below a minimum terminal size (80x30)
  • Render each option as one clipped terminal row, which prevents wrapping (which skews the scroll calculations)
  • Keep highlighted command visible when resizing
  • Reset the scroll window when filtering changes
  • Add resize gating to prevent flickering
    • Defer rerender until size is stable for 100ms (prevents constant flickering while resizing)
    • Redraw screen once upon final dimension

After this PR:

Screen.Recording.2026-09-29.at.8.28.45.PM.mov

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update
  • Other (please describe):

Testing

How have you tested the change?

  • bun run test (x pass, 0 fail)
  • I ran npm run test:unit and npm run test:integ
  • I ran npm run typecheck
  • I ran npm run lint
  • If I modified src/assets/, I ran npm run test:update-snapshots and committed the updated snapshots

Checklist

  • I have read the CONTRIBUTING document
  • I have added any necessary tests that prove my fix is effective or my feature works
  • I have updated the documentation accordingly
  • I have added an appropriate example to the documentation to outline the feature, or no new docs are needed
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the
terms of your choice.

@github-actions github-actions Bot added the size/m PR size: M label Sep 30, 2026
@agentcore-devx-automation agentcore-devx-automation Bot added agentcore-harness-reviewing AgentCore Harness review in progress claude-security-reviewing Claude Code /security-review in progress labels Sep 30, 2026
@agentcore-devx-automation

Copy link
Copy Markdown
Contributor

Claude Security Review: no high-confidence findings. (run)

@agentcore-devx-automation agentcore-devx-automation Bot removed the claude-security-reviewing Claude Code /security-review in progress label Sep 30, 2026

@agentcore-devx-automation agentcore-devx-automation Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AgentCore Harness Review

Verdict: Looks good

Nice refactor. The createResizeGate design is clean — coalescing shrink events until the gesture settles addresses the wrap-and-repaint race that the old clearScreenOnNarrowing could only mitigate, and routing Ink's resize listeners through an in-process EventEmitter while proxying dimensions keeps Ink from seeing intermediate shrink sizes. The traces I walked (shrink, shrink→grow, grow→shrink, no-op cycle) all publish exactly once at the final dimensions, and the ERASE happens before the emit so Ink's listener repaints on a cleared screen.

A few non-blocking observations for your consideration:

  • useScrollWindow is now unused (src/components/scrollWindow.tsx:86-95). Since CommandMenuBody now derives windowStart from highlight on every render, the hook that persisted start across renders is dead code. Worth removing (or leaving with a comment) so future readers don't wonder which is canonical.
  • Intentional UX change in scroll behavior? The new windowStart = max(0, highlight - floor(menuHeight/2)) (RouterScreen.tsx:260) recenters on the highlight every render, whereas useScrollWindow previously kept the window stable and scrolled only enough to keep the highlight visible. On arrow-key navigation this means every keystroke past the midpoint shifts every row, versus the older "scroll only when leaving the viewport." If that's the intended UX given the commit message, fine — flagging in case it wasn't.
  • BANNER_ROWS = 4 in RouterScreen.tsx:19 duplicates structural knowledge of BrandBanner (3 logo rows + divider). Since you already export MIN_BANNER_COLUMNS/ROWS from BrandBanner.tsx, consider exporting the banner height there too so it can't drift.

Resize test (tui.test.tsx) exercises the settle+coalesce behavior well; the new RouterScreen test for banner hide/restore is a good addition.

@agentcore-devx-automation agentcore-devx-automation Bot removed the agentcore-harness-reviewing AgentCore Harness review in progress label Sep 30, 2026
@codecov-commenter

codecov-commenter commented Sep 30, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.77419% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.24%. Comparing base (3e54f0c) to head (c247255).

Files with missing lines Patch % Lines
src/tui/resize.ts 92.13% 7 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           refactor    #2467      +/-   ##
============================================
- Coverage     97.26%   97.24%   -0.03%     
============================================
  Files           615      615              
  Lines         43665    43804     +139     
============================================
+ Hits          42471    42596     +125     
- Misses         1194     1208      +14     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread src/components/Layout.tsx Outdated
const { columns, rows } = useWindowSize();
const bannerVisible =
Boolean(banner) && !hideBanner && columns >= bannerMinColumns && rows >= bannerMinRows;
const contentRows = Math.max(0, rows - FRAME_ROWS - (bannerVisible ? bannerHeight : 0));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] Could we account for wrapped breadcrumbs and descriptions when calculating contentRows? At 40×15, scrolling the root menu to update hides the selected command because FRAME_ROWS underestimates the header height.

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.

Updated this PR. We now are measuring the breadcrumb headers actual rendered height, including wrapped breadcrumbs and descriptions.This value is subtracted when calculating contentRows. Added a regression test to cover this case as well

Comment thread src/tui/resize.ts

const publish = (clear: boolean) => {
const changed = columns !== pendingColumns || rows !== pendingRows;
if (!changed) return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] Could we force a full repaint after a shrink, even if the final size matches the cached one? Resizing 100×40 → 100×15 → 100×40 with 25ms between changes skips all output here, leaving the header and selection missing until another state change.

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.

Good callout. Updated PR so we force Ink’s cache to be invalidated when a shrink gesture lands at its original dimensions. We then follow with a repaint to the actual final size. Added a regression test covering this exact scenario

Comment thread src/tui/resize.ts
addListener("prependOnceListener", event, listener);
}
if (property === "off" || property === "removeListener") {
return removeListener;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It looks like this is only used in tests. If so, can it be put in a test util file? That would make the intent clearer.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Oh wait, it's used in the root.

@github-actions github-actions Bot added size/m PR size: M and removed size/m PR size: M labels Sep 30, 2026
@agentcore-devx-automation agentcore-devx-automation Bot added the claude-security-reviewing Claude Code /security-review in progress label Sep 30, 2026
@agentcore-devx-automation

Copy link
Copy Markdown
Contributor

Claude Security Review: no high-confidence findings. (run)

@agentcore-devx-automation agentcore-devx-automation Bot removed the claude-security-reviewing Claude Code /security-review in progress label Sep 30, 2026
@github-actions github-actions Bot added size/m PR size: M and removed size/m PR size: M labels Sep 30, 2026
@agentcore-devx-automation agentcore-devx-automation Bot added the claude-security-reviewing Claude Code /security-review in progress label Sep 30, 2026
@agentcore-devx-automation

Copy link
Copy Markdown
Contributor

Claude Security Review: no high-confidence findings. (run)

@agentcore-devx-automation agentcore-devx-automation Bot removed the claude-security-reviewing Claude Code /security-review in progress label Sep 30, 2026
@nborges-aws
nborges-aws merged commit 95ba6ee into refactor Sep 30, 2026
18 checks passed
@nborges-aws
nborges-aws deleted the optimize-small-terminals branch September 30, 2026 01:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m PR size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants