Skip to content

Windows TUI Height Fix #10

@svfolder

Description

@svfolder

Windows TUI Height Fix — Bug Report for Developers

Problem Description

When running the TUI (qwen-proxy serve without --headless) on Windows 10, the following issues occur:

  1. Top menu bar (header/status) is not visible — content renders above the visible area of the terminal window
  2. Scrollbar appears on every data refresh — content overflows the visible area, causing the terminal to show a scrollbar
  3. The issue reproduces consistently on every data update cycle (every second)

Root Cause

On Windows, the terminal window title bar occupies an additional line that is counted in process.stdout.rows but is not visible to the user. The TUI uses process.stdout.rows directly without compensating for this offset, causing the calculated content height to exceed the actual visible area by multiple lines.

Additionally, when the TUI outputs exactly termRows lines, the Windows terminal creates a scroll overflow. Reducing the output by several extra lines prevents this behavior.

Applied Changes

All changes were applied directly in the compiled (bundled) files of the global npm installation.

Installation path: C:\Users\NEO\AppData\Roaming\npm\node_modules\qwen-proxy\dist\src\tui2\


1. state.js — Initial Viewport Initialization

File: dist/src/tui2/state.js
Lines: 28-33

Before:

function initialViewport() {
    return {
        cols: process.stdout.columns ?? 120,
        rows: process.stdout.rows ?? 40,
    };
}

After:

const WINDOWS_TITLE_OFFSET = typeof process !== "undefined" && process.platform === "win32" ? 1 : 0;
function initialViewport() {
    return {
        cols: process.stdout.columns ?? 120,
        rows: (process.stdout.rows ?? 40) - WINDOWS_TITLE_OFFSET,
    };
}

2. main.js — Resize and Periodic Tick Handlers

File: dist/src/tui2/main.js
Lines: 318-331

Before:

process.on("resize", () => {
    dispatch({
        type: "set-viewport",
        cols: process.stdout.columns ?? currentState.viewportCols,
        rows: process.stdout.rows ?? currentState.viewportRows,
    });
});
tickTimer = setInterval(() => {
    dispatch({ type: "tick", nowMs: Date.now() });
    dispatch({
        type: "set-viewport",
        cols: process.stdout.columns ?? currentState.viewportCols,
        rows: process.stdout.rows ?? currentState.viewportRows,
    });

After:

const WINDOWS_TITLE_OFFSET = process.platform === "win32" ? 1 : 0;
process.on("resize", () => {
    dispatch({
        type: "set-viewport",
        cols: process.stdout.columns ?? currentState.viewportCols,
        rows: (process.stdout.rows ?? currentState.viewportRows) - WINDOWS_TITLE_OFFSET,
    });
});
tickTimer = setInterval(() => {
    dispatch({ type: "tick", nowMs: Date.now() });
    dispatch({
        type: "set-viewport",
        cols: process.stdout.columns ?? currentState.viewportCols,
        rows: (process.stdout.rows ?? currentState.viewportRows) - WINDOWS_TITLE_OFFSET,
    });

3. app.js — Render Function

File: dist/src/tui2/app.js
Lines: ~764-776

Before:

const termRows = terminal?.rows ?? state.viewportRows ?? 40;
const sbW = this.sidebarWidth();
const mainW = Math.max(20, width - sbW - 1);
const sidebarLines = this.renderSidebar(sbW, termRows);
const header = this.renderHeader(mainW);
const footer = this.renderFooter(mainW);
const contentRows = Math.max(0, termRows - 2);

After:

const termRows = terminal?.rows ?? state.viewportRows ?? 40;
const windowsTitleOffset = process.platform === "win32" ? 1 : 0;
const sbW = this.sidebarWidth();
const mainW = Math.max(20, width - sbW - 1);
const sidebarLines = this.renderSidebar(sbW, termRows);
const header = this.renderHeader(mainW);
const footer = this.renderFooter(mainW);
const windowsExtra = process.platform === "win32" ? 6 : 0;
const contentRows = Math.max(0, termRows - 2 - windowsTitleOffset - windowsExtra);

Explanation:

  • windowsTitleOffset (-1 line): Compensates for the invisible title bar line on Windows counted in process.stdout.rows
  • windowsExtra (-6 lines): Prevents output overflow — on Windows, the terminal creates a scroll when the number of output lines equals or approaches termRows. Empirically determined value of 6 fully eliminates the issue.
  • Total for Windows: contentRows = termRows - 8 instead of termRows - 2

Recommendations for a Permanent Fix

Instead of hardcoding process.platform === "win32", consider:

  1. Use the @mariozechner/pi-tui library API — check if it provides automatic height compensation for Windows. If available, use its API to get the real visible height.

  2. Alternative approach — measure the actual visible area experimentally (e.g., write a character to the last line and check if it's visible) instead of platform-dependent code.

  3. If platform-dependent approach is acceptable — extract the constant into a single config file and apply it everywhere contentRows/viewportRows is calculated.

Source Files (for Build)

These changes should be applied to the source files (before compilation into dist/):

Bundled File Probable Source File (estimated)
dist/src/tui2/state.js src/tui2/state.ts
dist/src/tui2/main.js src/tui2/main.ts
dist/src/tui2/app.js src/tui2/app.tsx or src/tui2/app.ts

Note About Local Repository

The local repository at C:\EX2025\STORM\qwen-proxy does not contain TUI source files. The TUI code exists only in the global npm installation of the qwen-proxy package. The local project contains only the proxy server portion (src/index.js, src/qwen/, src/utils/).

The fixes were applied directly to the bundled files of the global installation:

  • C:\Users\NEO\AppData\Roaming\npm\node_modules\qwen-proxy\dist\src\tui2\state.js
  • C:\Users\NEO\AppData\Roaming\npm\node_modules\qwen-proxy\dist\src\tui2\main.js
  • C:\Users\NEO\AppData\Roaming\npm\node_modules\qwen-proxy\dist\src\tui2\app.js

Testing

After applying the fix to source code and rebuilding:

  • Test on Windows 10 with Windows Terminal and standard cmd.exe
  • Test on Linux/macOS to ensure the fix does not affect other platforms
  • Verify that content correctly recalculates on window resize

Verified Working

The fix with contentRows = termRows - 8 for Windows has been tested and confirmed working — the header is fully visible and no unwanted scrolling occurs during data refresh cycles.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions