Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8f5eb98
fix(cli): default to virtualized terminal history
ZevGit Jun 23, 2026
738cac3
Merge branch 'main' into fix/cli-scroll-flicker
wenshao Jun 24, 2026
b35fef7
Merge remote-tracking branch 'upstream/main' into fix/cli-scroll-flicker
ZevGit Jun 25, 2026
604e733
fix(cli): remove redundant alternate screen exit handler
ZevGit Jun 26, 2026
cc5d436
Merge remote-tracking branch 'upstream/main' into fix/cli-scroll-flicker
ZevGit Jun 26, 2026
22f100f
Merge branch 'main' into fix/cli-scroll-flicker
wenshao Jun 27, 2026
769e196
fix(cli): merge main into scroll-flicker branch, resolving useTermina…
qwen-code-dev-bot Jul 4, 2026
8632a62
fix(cli): keep non-interactive output off VP mode
ZevGit Jul 5, 2026
975d216
fix(cli): stabilize VP tests in CI environments
ZevGit Jul 6, 2026
874fb6c
Merge remote-tracking branch 'upstream/main' into fix/cli-scroll-flicker
ZevGit Jul 6, 2026
50187da
test(cli): resolve SDK daemon source in vitest
ZevGit Jul 6, 2026
81daecf
fix(cli): normalize CI env checks for VP mode
ZevGit Jul 6, 2026
e1cc1c4
Merge remote-tracking branch 'upstream/main' into fix/cli-scroll-flicker
ZevGit Jul 6, 2026
ddc0c31
fix(cli): keep default VP mouse interactions enabled
ZevGit Jul 6, 2026
6953ae4
Merge branch 'main' into fix/cli-scroll-flicker
ZevGit Jul 6, 2026
c4485e2
fix(cli): resolve merge conflicts with main for virtualized terminal …
qwen-code-dev-bot Jul 9, 2026
999a609
Merge branch 'main' into fix/cli-scroll-flicker
wenshao Jul 10, 2026
9e0919b
Merge branch 'main' into fix/cli-scroll-flicker
wenshao Jul 10, 2026
f4cdecc
fix(cli): align VP mouse behavior with runtime state
ZevGit Jul 10, 2026
cc24b0a
Merge remote-tracking branch 'upstream/main' into fix/cli-scroll-flicker
ZevGit Jul 13, 2026
0c9df2b
fix(cli): stabilize virtual viewport runtime state
ZevGit Jul 13, 2026
a22120a
test(cli): cover virtual viewport fallbacks
ZevGit Jul 13, 2026
ad92da5
Merge remote-tracking branch 'upstream/main' into fix/cli-scroll-flicker
ZevGit Jul 14, 2026
cef51a6
docs(cli): clarify virtual viewport requirements
ZevGit Jul 14, 2026
66a1499
Merge remote-tracking branch 'upstream/main' into fix/cli-scroll-flicker
ZevGit Jul 24, 2026
2a097de
Merge branch 'main' into fix/cli-scroll-flicker
ZevGit Jul 24, 2026
d18f38a
Merge upstream/main into fix/cli-scroll-flicker
ZevGit Jul 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions docs/design/virtual-viewport/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ Deferred to follow-up PRs:

- **Scrollbar drag + click-to-position** — needs screen-absolute element coords, blocked on a stock-ink-7 limitation (see V.4 / V.7).
- **In-app `/` search** — claude-code's `TranscriptSearchBar` pattern (V.5).
- **Alternate-buffer mode** — `contexts/ScrollProvider.tsx`-style focus / lock, with full alt-screen takeover (V.6).
- **Dedicated alternate-buffer setting** — VP already enters alternate screen; revisit a separate toggle only if compatibility reports require it.

### Setting (V.2)

Expand All @@ -132,9 +132,10 @@ ui: {
/**
* Enables virtualized history rendering for long conversations.
* When true, only items in the visible viewport are rendered through React;
* scrolled-out items remain in the terminal scrollback buffer.
* scrolled-out items stay in the in-app scrollback model instead of the
* host terminal scrollback buffer.
*
* Default: false. Opt-in until proven stable on long conversations.
* Default: true. Users can opt out if they prefer host terminal scrollback.
*/
useTerminalBuffer?: boolean; // alias kept compat with gemini-cli
}
Expand All @@ -143,7 +144,7 @@ ui: {
`MainContent.tsx` reads the setting and switches paths:

```tsx
const useTerminalBuffer = uiState.settings?.ui?.useTerminalBuffer ?? false;
const useTerminalBuffer = uiState.settings?.ui?.useTerminalBuffer ?? true;

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.

This snippet was updated for the new default, but it no longer matches the implementation this same PR ships: MainContent.tsx doesn't read settings here anymore — it reads the startup-frozen uiState.useTerminalBuffer (const useVirtualScroll = uiState.useTerminalBuffer;), which AppContainer computes once via shouldUseVirtualViewport(settings.merged.ui?.useTerminalBuffer, config.getScreenReader()) in a useState initializer.

As written, the snippet still documents a live, ungated settings read — the exact shape this PR deliberately removes (no screen-reader gate, mid-session flippable). A contributor following the doc would reintroduce a read that can desync from the alternateScreen decision made once at render().

Suggest updating the snippet to the real shape (AppContainer's frozen shouldUseVirtualViewport(...) + MainContent reading uiState.useTerminalBuffer), and mentioning the screen-reader gate and restart-only semantics.

🤖 Generated with Claude Code — Claude Fable 5

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 8632a62. The design doc now shows the actual startup-frozen AppContainer decision using shouldUseVirtualViewport(..., isInteractiveTerminal()), and MainContent reading uiState.useTerminalBuffer. It also documents the screen-reader/non-interactive fallback and restart-only behavior.


if (useTerminalBuffer) {
return <ScrollableList .../>; // virtualized
Expand All @@ -152,7 +153,7 @@ if (useTerminalBuffer) {
return <Static .../>; // existing path, untouched
```

The legacy `<Static>` path stays as-is — no regression risk for users who don't opt in.
The legacy `<Static>` path stays available for users who explicitly opt out.

## 6. Key adaptations from gemini-cli source

Expand Down Expand Up @@ -316,10 +317,10 @@ Same pattern in qwen-code. Required for virtualization to actually skip re-rende
| **V.3** | test(integration): capture-suite regressions for streaming / resize / shell | port 3 capture scripts from PR #3663 | ~2000 (test-only) | #4146 | pending |
| **V.4** | feat(cli): scrollbar drag + click-to-position | SGR mouse hit-test on scrollbar column. Needs screen-absolute coords — either upstream `getBoundingBox` to ink 7 or own yoga walker. Auto-hide animation already shipped in #4146. | ~400 | #4146 | deferred — coord blocker |
| **V.5** | feat(cli): in-app `/` search | viewport-bound highlight + n/N navigation (claude-code's `TranscriptSearchBar` pattern) | ~300 | #4146 | deferred |
| **V.6** | feat(cli): alternate-buffer mode (full alt-screen takeover) | additional setting `ui.useAlternateBuffer` | ~500 | #4146 | deferred — separate UX decision required |
| **V.6** | feat(cli): dedicated alternate-buffer toggle | no separate setting planned for the default flow; VP already enters alternate screen, revisit only if compatibility reports require it | — | #4146 | deferred — compatibility-driven only |
| **V.7** | research: preserve host terminal scrollback (dual-write) | `@jrichman/ink`'s `overflowToBackbuffer` is fork-only. Options: upstream PR to ink 7, own dual-write, or accept loss. Investigation. | — | #4146 | structurally blocked on stock ink 7 |

V.3 (integration tests) is the remaining critical-path item before flipping the default. V.4–V.6 close the remaining gemini-cli-parity gaps; V.7 is open research because the underlying ink prop we'd need (`overflowToBackbuffer`) only exists in gemini-cli's `@jrichman/ink` fork.
V.3 (integration tests) remains desirable for long-session regression coverage but is no longer a gating prerequisite for the default flip. V.4–V.6 close the remaining gemini-cli-parity gaps; V.7 is open research because the underlying ink prop we'd need (`overflowToBackbuffer`) only exists in gemini-cli's `@jrichman/ink` fork.

## 8. Verification plan

Expand All @@ -342,10 +343,10 @@ End-to-end (after V.3):
## 9. Open questions / decisions needed

1. **Setting name**: `ui.useTerminalBuffer` (gemini-cli compat) vs `ui.virtualizedHistory` (more descriptive)?
2. **Default value**: ship as `false` (opt-in) or stage rollout via env var first?
2. **Default value**: resolved as `true` (default-on) with `false` as an explicit opt-out.
3. **Static-item heuristic**: gemini-cli marks only `header` as static. Should we also mark completed Gemini messages, tool results that are no longer in `pendingHistoryItems`, etc.?
4. **Mouse support**: gemini-cli's `ScrollProvider` includes mouse drag for scrollbar. Worth porting now or skip until V.4?
5. **Compatibility with #3905**: ~~PR #3905 (Ctrl+O freeze fix) is open and modifies the same `MainContent.tsx`. Coordinate merge order — likely V.2 rebases on top of #3905.~~ **Resolved**: #3905's progressive-replay landed in `main` and is preserved in the legacy `<Static>` branch of `MainContent.tsx`; the VP branch supersedes it for opt-in users because the freeze trigger (full Static remount) no longer applies.
5. **Compatibility with #3905**: ~~PR #3905 (Ctrl+O freeze fix) is open and modifies the same `MainContent.tsx`. Coordinate merge order — likely V.2 rebases on top of #3905.~~ **Resolved**: #3905's progressive-replay landed in `main` and is preserved in the legacy `<Static>` branch of `MainContent.tsx`; the VP branch supersedes it for default users because the freeze trigger (full Static remount) no longer applies.
6. **Compatibility with `chore/re-upgrade-ink-7-0-3`**: PR #4146 stacks on it. After #4119 (the ink 7.0.3 re-upgrade PR) merges to `main`, PR #4146's base will re-target to `main`.

## 10. Risks
Expand All @@ -361,7 +362,7 @@ End-to-end (after V.3):
## 11. Approval checklist

- [x] Architectural direction approved — port from gemini-cli (§4)
- [x] Setting name + default decided — `ui.useTerminalBuffer`, default `false` (opt-in)
- [x] Setting name + default decided — `ui.useTerminalBuffer`, default `true` (opt-out)
- [x] Static-item heuristic — `isStaticItem={(item) => item.id > 0}` (completed history items)
- [x] Mouse-support scope — deferred to V.4; keyboard-only scroll in #4146
- [x] Merge ordering with #3905 (§9.5) — #3905 already in `main`; #4146 preserves the legacy progressive-replay path and supersedes it only for VP users
Expand Down
4 changes: 2 additions & 2 deletions docs/users/reference/keyboard-shortcuts.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ This document lists the available keyboard shortcuts in Qwen Code.

## History scrollback

Active only when `ui.useTerminalBuffer` is enabled (Settings → UI → Virtualized History). In that mode conversation history is rendered inside an in-app viewport instead of the host terminal scrollback, so the keys below replace the terminal's native scroll.
Active when `ui.useTerminalBuffer` is enabled (Settings → UI → Virtualized History) and screen reader mode is off, which is the default for non-screen-reader sessions. In that mode conversation history is rendered inside an in-app viewport instead of the host terminal scrollback, so the keys below replace the terminal's native scroll.

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 activation description omits the terminal-compatibility gate. Even with ui.useTerminalBuffer enabled and screen-reader mode off, shouldUseVirtualViewport() falls back to append-only history when stdout is not a TTY, CI is active, or TERM=dumb, so these shortcuts are unavailable in those sessions. Please document the compatible interactive-terminal requirement so this page matches the runtime.

Suggested change
Active when `ui.useTerminalBuffer` is enabled (Settings → UI → Virtualized History) and screen reader mode is off, which is the default for non-screen-reader sessions. In that mode conversation history is rendered inside an in-app viewport instead of the host terminal scrollback, so the keys below replace the terminal's native scroll.
Active when `ui.useTerminalBuffer` is enabled (Settings → UI → Virtualized History), screen reader mode is off, and qwen-code is running in a compatible interactive terminal (TTY stdout, not CI, and `TERM` is not `dumb`), which is the default for ordinary non-screen-reader sessions. In that mode conversation history is rendered inside an in-app viewport instead of the host terminal scrollback, so the keys below replace the terminal's native scroll.

— Codex $qreview 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.

Addressed in cef51a6. The shortcut documentation now states the interactive-terminal gate explicitly, including TTY stdout, inactive CI, and TERM != dumb.


| Shortcut | Description |
| --------------- | ---------------------------------------------------- |
Expand All @@ -87,7 +87,7 @@ When `ui.useTerminalBuffer` is on, the terminal forwards mouse events to qwen-co

Inside tmux, some terminals translate trackpad or wheel gestures into plain `Up Arrow` and `Down Arrow` sequences before qwen-code sees them. Those bytes are identical to real arrow-key presses, so qwen-code cannot tell whether you meant to scroll the viewport or navigate prompt history.

If trackpad scrolling changes the prompt history in tmux, enable `ui.useTerminalBuffer`; then use `Shift+Up` / `Shift+Down`, or the mouse wheel when tmux forwards wheel events to the app. If you prefer host scrollback, adjust your tmux mouse bindings for wheel events.
If trackpad scrolling changes the prompt history in tmux, make sure `ui.useTerminalBuffer` is enabled; then use `Shift+Up` / `Shift+Down`, or the mouse wheel when tmux forwards wheel events to the app. If you prefer host scrollback, adjust your tmux mouse bindings for wheel events.

## IDE Integration

Expand Down
2 changes: 1 addition & 1 deletion docs/users/support/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ This guide provides solutions to common issues and debugging tips, including top
- **Trackpad scrolling in tmux changes prompt history instead of scrolling the conversation**
- **Issue:** In a tmux session, trackpad or wheel scrolling may cycle through previous prompts, similar to pressing `Up Arrow` or `Down Arrow`.
- **Cause:** tmux can translate wheel gestures into plain arrow-key sequences. Those sequences are indistinguishable from real arrow-key presses by the time qwen-code receives them.
- **Solution:** Enable `ui.useTerminalBuffer`; then use `Shift+Up` / `Shift+Down`, or the mouse wheel when tmux forwards wheel events to the app. If you prefer host scrollback, adjust your tmux mouse bindings for wheel events.
- **Solution:** If screen reader mode is disabled, make sure `ui.useTerminalBuffer` is enabled; then use `Shift+Up` / `Shift+Down`, or the mouse wheel when tmux forwards wheel events to the app. If you prefer host scrollback, adjust your tmux mouse bindings for wheel events.

## IDE Companion not connecting

Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/config/settingsSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,9 +366,9 @@ describe('SettingsSchema', () => {
getSettingsSchema().ui.properties.useTerminalBuffer;
expect(useTerminalBuffer).toBeDefined();
expect(useTerminalBuffer.type).toBe('boolean');
expect(useTerminalBuffer.default).toBe(false);
expect(useTerminalBuffer.default).toBe(true);
expect(useTerminalBuffer.showInDialog).toBe(true);
expect(useTerminalBuffer.requiresRestart).toBe(false);
expect(useTerminalBuffer.requiresRestart).toBe(true);
});

it('should expose response tokens/sec as an opt-in UI setting', () => {
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1005,10 +1005,10 @@ const SETTINGS_SCHEMA = {
type: 'boolean',
label: 'Virtualized History (reduces flicker on long sessions)',
category: 'UI',
requiresRestart: false,
default: false,
requiresRestart: true,
default: true,

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 ddc0c31. InputPrompt, BaseSelectionList, and useMouseEvents now prefer the frozen VP decision from UIStateContext, with raw settings only as a fallback for usage outside UIStateContext. Added regression coverage for the unset raw setting across all three mouse paths.

description:
'Render conversation history in an in-app scrollable viewport instead of the terminal scrollback buffer. Recommended if you see flicker, scroll-storm, or interface freeze on long sessions, after Ctrl+O, after Ctrl+E / Ctrl+F (expand), after window resize, or when alt-tabbing back. Scroll with Shift+↑/↓ (line), PgUp/PgDn (page), Ctrl+Home/End (top/bottom), or the mouse wheel. Does NOT use the host terminal scrollback while enabled; for native text selection, hold Shift (or Option on macOS) while dragging.',
'Render conversation history in an in-app scrollable viewport instead of the terminal scrollback buffer. Enabled by default to avoid flicker, scroll-storm, and interface freeze on long sessions, after Ctrl+O, after Ctrl+E / Ctrl+F (expand), after window resize, or when alt-tabbing back. Screen reader mode uses append-only terminal output instead. Scroll with Shift+↑/↓ (line), PgUp/PgDn (page), Ctrl+Home/End (top/bottom), or the mouse wheel. Does NOT use the host terminal scrollback while enabled; for native text selection, hold Shift (or Option on macOS) while dragging.',
showInDialog: true,
},
shellOutputMaxLines: {
Expand Down
119 changes: 118 additions & 1 deletion packages/cli/src/gemini.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1253,8 +1253,22 @@ describe('startInteractiveUI', () => {
render: vi.fn().mockReturnValue({ unmount: vi.fn() }),
}));

let initialExitListeners: NodeJS.ExitListener[] = [];

beforeEach(() => {
vi.clearAllMocks();
initialExitListeners = process.listeners('exit') as NodeJS.ExitListener[];
});

afterEach(() => {
const currentExitListeners = process.listeners(
'exit',
) as NodeJS.ExitListener[];
for (const listener of currentExitListeners) {
if (!initialExitListeners.includes(listener)) {
process.removeListener('exit', listener);
}
}
});

it('should render the UI with proper React context and exitOnCtrlC disabled', async () => {
Expand Down Expand Up @@ -1284,13 +1298,116 @@ describe('startInteractiveUI', () => {
expect(options).toEqual({
exitOnCtrlC: false,
isScreenReaderEnabled: false,
alternateScreen: false,
alternateScreen: true,
});

// Verify React element structure is valid (but don't deep dive into JSX internals)
expect(reactElement).toBeDefined();
});

it('should not use alternate screen when VP mode is explicitly disabled', async () => {
const { render } = await import('ink');
const renderSpy = vi.mocked(render);
const legacySettings = {
...mockSettings,
merged: {
...mockSettings.merged,
ui: {
...mockSettings.merged.ui,
useTerminalBuffer: false,
},
},
} as LoadedSettings;

const mockInitializationResult = {
authError: null,
themeError: null,
shouldOpenAuthDialog: false,
geminiMdFileCount: 0,
};

await startInteractiveUI(
mockConfig,
legacySettings,
mockStartupWarnings,
mockWorkspaceRoot,
mockInitializationResult,
);

const [, options] = renderSpy.mock.calls[0];
expect(options).toMatchObject({ alternateScreen: false });
});

it('should not use alternate screen in screen reader mode when VP mode is unset', async () => {

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] Test gap: the screen-reader guard is tested when useTerminalBuffer is unset (defaults to true), but not when it's explicitly true. Add a test with useTerminalBuffer: true + getScreenReader(): true to cover the true && !true branch:

it('should not use alternate screen in screen reader mode even when VP mode is explicitly enabled', async () => {
  // useTerminalBuffer: true, getScreenReader: () => true
  // expect alternateScreen: false
});

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

Covered in 8f5eb98.

Added the explicit useTerminalBuffer: true + screen-reader case for startInteractiveUI, asserting alternateScreen remains false.

const { render } = await import('ink');
const renderSpy = vi.mocked(render);
const screenReaderConfig = {
...mockConfig,
getScreenReader: () => true,

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.

Added in a22120a. startInteractiveUI now has a real-process-env integration test with stdout.isTTY=true and CI=true, asserting alternateScreen=false. I also corrected teardown ordering so vi.stubEnv values are removed before the original CI environment snapshot is restored, preventing cross-test env leakage on GitHub Actions.

} as Config;

const mockInitializationResult = {
authError: null,
themeError: null,
shouldOpenAuthDialog: false,
geminiMdFileCount: 0,
};

await startInteractiveUI(
screenReaderConfig,
mockSettings,
mockStartupWarnings,
mockWorkspaceRoot,
mockInitializationResult,
);

const [, options] = renderSpy.mock.calls[0];
expect(options).toMatchObject({
isScreenReaderEnabled: true,
alternateScreen: false,
});
});

it('should not use alternate screen in screen reader mode even when VP mode is explicitly enabled', async () => {
const { render } = await import('ink');
const renderSpy = vi.mocked(render);
const screenReaderConfig = {
...mockConfig,
getScreenReader: () => true,
} as Config;
const vpSettings = {
...mockSettings,
merged: {
...mockSettings.merged,
ui: {
...mockSettings.merged.ui,
useTerminalBuffer: true,
},
},
} as LoadedSettings;

const mockInitializationResult = {
authError: null,
themeError: null,
shouldOpenAuthDialog: false,
geminiMdFileCount: 0,
};

await startInteractiveUI(
screenReaderConfig,
vpSettings,
mockStartupWarnings,
mockWorkspaceRoot,
mockInitializationResult,
);

const [, options] = renderSpy.mock.calls[0];
expect(options).toMatchObject({
isScreenReaderEnabled: true,
alternateScreen: false,
});
});

it('should perform all startup tasks in correct order', async () => {
const { getCliVersion } = await import('./utils/version.js');
const { checkForUpdates } = await import('./ui/utils/updateCheck.js');
Expand Down
104 changes: 103 additions & 1 deletion packages/cli/src/ui/AppContainer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
type Mock,
} from 'vitest';
import { render, cleanup } from 'ink-testing-library';
import { useContext, act } from 'react';
import { useContext, useState, act } from 'react';
import {
AppContainer,
dedupeNewestFirst,
Expand Down Expand Up @@ -396,6 +396,7 @@ describe('AppContainer State Management', () => {
ui: {
showStatusInTitle: false,
hideWindowTitle: false,
useTerminalBuffer: false,
},
},
setValue: vi.fn(),
Expand Down Expand Up @@ -713,6 +714,107 @@ describe('AppContainer State Management', () => {
);
});

it('defaults to VP mode when useTerminalBuffer is unset', () => {

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.

Added in a22120a. AppContainer now has an integration test that sets process.stdout.isTTY=false with useTerminalBuffer unset and asserts the frozen UI state stays on the Static path. The test environment restores the original TTY descriptor afterward.

const defaultSettings = {
merged: {
hideTips: false,
theme: 'default',
ui: {
showStatusInTitle: false,
hideWindowTitle: false,
},
},
setValue: vi.fn(),
} as unknown as LoadedSettings;

render(
<AppContainer
config={mockConfig}
settings={defaultSettings}
version="1.0.0"
initializationResult={mockInitResult}
/>,
);

expect(capturedUIState.useTerminalBuffer).toBe(true);
});

it('keeps screen reader mode on the Static path when useTerminalBuffer is unset', () => {
vi.spyOn(mockConfig, 'getScreenReader').mockReturnValue(true);
const defaultSettings = {
merged: {
hideTips: false,
theme: 'default',
ui: {
showStatusInTitle: false,
hideWindowTitle: false,
},
},
setValue: vi.fn(),
} as unknown as LoadedSettings;

render(
<AppContainer
config={mockConfig}
settings={defaultSettings}
version="1.0.0"
initializationResult={mockInitResult}
/>,
);

expect(capturedUIState.useTerminalBuffer).toBe(false);
});

it('locks terminal buffer mode for the running session', () => {
const vpSettings = {
merged: {
hideTips: false,
theme: 'default',
ui: {
showStatusInTitle: false,
hideWindowTitle: false,
useTerminalBuffer: true,
},
},
setValue: vi.fn(),
} as unknown as LoadedSettings;
const legacySettings = {
merged: {
hideTips: false,
theme: 'default',
ui: {
showStatusInTitle: false,
hideWindowTitle: false,
useTerminalBuffer: false,
},
},
setValue: vi.fn(),
} as unknown as LoadedSettings;

vi.spyOn(mockConfig, 'initialize').mockResolvedValue(undefined);
let updateSettings!: (settings: LoadedSettings) => void;
function Wrapper() {
const [settings, setSettings] = useState(vpSettings);
updateSettings = setSettings;
return (
<AppContainer
config={mockConfig}
settings={settings}
version="1.0.0"
initializationResult={mockInitResult}
/>
);
}

render(<Wrapper />);

expect(capturedUIState.useTerminalBuffer).toBe(true);

act(() => updateSettings(legacySettings));

expect(capturedUIState.useTerminalBuffer).toBe(true);
});

// #4891 changed the resize contract: width changes now trigger ONE full
// clearTerminal after RESIZE_REPAINT_SETTLE_MS (trailing-edge debounce),
// instead of never (#3967) or per-event (pre-#3967). This test pins the
Expand Down
Loading
Loading