Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions packages/tui/src/tui/engine/LOCAL_CHANGES.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,8 @@
},
{
"path": "tui-main-screen.ts",
"currentSha256": "146fd4222ae5a48d1f439de2f280b111789151a30c1bdd2735e32fff07329b9d",
"changeIds": ["L005", "L017", "L027", "L033", "L034"],
"currentSha256": "6970c4bac059263fc1ec420fa07362b7bcc1838fd381dbaa5a61180c15bb4787",
"changeIds": ["L005", "L017", "L027", "L033", "L034", "L036"],
"upstreamCommit": "6c4f360264397c59801f6da2bdac13e3b1fcbe91",
"reason": "Keep strict TypeScript fixes and stream full and differential renders through Pi's bounded terminal writer. 缩放期间仅重绘可见尾部 常规模式在差分比较前剥离行首 OSC 133 zone 标记。 内容收缩或历史内容变化触发回退重绘时仅更新可见区域。",
"behaviorImpact": "Large regular-mode renders preserve output order without constructing a single V8-limited terminal string. 150ms 后及退出前重放完整投影历史。 内部导航标记不再写入终端,避免 iTerm2 左侧箭头和命令条噪声。 Session 结束后的状态更新不再发送 ESC 3J 清除原生滚动历史,当前视口仍显示最新内容。"
Expand Down
8 changes: 8 additions & 0 deletions packages/tui/src/tui/engine/LOCAL_CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,11 @@ Remove `L024` when the selected Pi baseline natively matches legacy-terminal `Ct
- Adaptation: the original contribution widened hits over two live content columns. The reserved gutter prevents those presses from swallowing content clicks or text selection. `chat-layout.ts` opts into the three-column gutter.
- Evidence: `test/unit/tui-scrollbar-interaction.test.ts` drives SGR press, motion, release and wheel events through VirtualTerminal, covering track jumps, thumb grabs, narrow terminals, content routing, selection, overlays and the actual fullscreen ChatLayout.
- Removal condition: the selected Pi baseline provides equivalent reserved-gutter track and drag interaction and MCode migrates to it.

## L036: Rebuild structural collapses across native scrollback

- Origin: follow-up to [PR #243](https://github.com/MiniMax-AI/minimax-code/pull/243).
- Product contract: collapsing transient content must retain the folded summary and every Composer row, even when they move above the previous viewport origin.
- Minimal difference: during a shrinking viewport-only redraw, compare the text of the scrolled prefix after stripping terminal sequences. If that prefix changed, rebuild the full projection. Tail-only shrink and style-only prefix changes continue to preserve native scrollback; pending resize replay retains its temporary tail view.
- Evidence: `test/unit/tui-engine-local-deltas.test.ts` covers stale transient rows, all Composer rows after a selector closes, and simultaneous shrink plus style-only changes without duplicate answer rows.
- Removal condition: the selected Pi baseline supplies equivalent structural-collapse handling.
12 changes: 8 additions & 4 deletions packages/tui/src/tui/engine/tui-main-screen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as fs from "node:fs";
import * as path from "node:path";
import { deleteKittyImage, isImageLine } from "./terminal-image.js";
import { type TUI, TuiBase, type TuiStopOptions } from "./tui.js";
import { visibleWidth } from "./utils.js";
import { stripTerminalSequences, visibleWidth } from "./utils.js";

const KITTY_SEQUENCE_PREFIX = "\x1b_G";
const MAX_RENDER_WRITE_CHARS = 1024 * 1024;
Expand Down Expand Up @@ -308,9 +308,13 @@ export class TuiMainScreen extends TuiBase implements TUI {
// previous viewport origin so rows already scrolled out are not painted twice,
// and growth still writes every row before it scrolls out. Resize previews are
// temporary: they show the new tail until the pending full history replay.
if (viewportOnly && !this.historyReplayPending && newLines.length <= prevViewportTop) {
// Nothing remains addressable on screen; rebuild with one consistent origin.
viewportOnly = false;
if (viewportOnly && !this.historyReplayPending) {
// A shrink can move new content (including the Composer) into immutable
// scrollback. Rebuild when that prefix changes, but ignore style-only edits.
const changedScrolledPrefix = newLines.length < this.previousLines.length &&
newLines.slice(0, prevViewportTop).some((line, index) =>
stripTerminalSequences(line) !== stripTerminalSequences(this.previousLines[index] ?? ""));
if (newLines.length <= prevViewportTop || changedScrolledPrefix) viewportOnly = false;
}
const start = viewportOnly
? this.historyReplayPending
Expand Down
56 changes: 50 additions & 6 deletions packages/tui/test/unit/tui-engine-local-deltas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ describe('MCode Pi Engine local deltas', () => {
}
});

it('preserves native scrollback when transient rows shrink above the viewport', async () => {
it('rebuilds when a transient collapse changes rows already in scrollback', async () => {
const terminal = new RecordingVirtualTerminal(40, 5);
const tui = new TuiMainScreen(terminal);
const component = new MutableLines();
Expand All @@ -81,13 +81,55 @@ describe('MCode Pi Engine local deltas', () => {

const writes = terminal.takeWrites();
expect(writes).toContain('\x1b[2J\x1b[H');
expect(writes).not.toContain('\x1b[3J');
expect(terminal.getScrollBuffer()).toContain('activity-1');
expect(terminal.getViewport()).toEqual(['composer', '', '', '', '']);
expect(writes).toContain('\x1b[3J');
expect(terminal.getScrollBuffer()).not.toContain('activity-1');
expect(terminal.getViewport()).toEqual(['header', 'status', 'composer', '', '']);
expect(terminal.getScrollBuffer().filter((line) => line === 'header')).toHaveLength(1);
expect(tui.fullRedraws).toBe(2);
});

it('keeps every composer row visible after a collapse crosses the viewport origin', async () => {
const terminal = new RecordingVirtualTerminal(40, 10);
const tui = new TuiMainScreen(terminal);
const component = new MutableLines();
const chat = Array.from({ length: 15 }, (_, index) => `Chat ${index}`);
const composer = ['Editor 0', 'Editor 1', `Editor 2${CURSOR_MARKER}`];
component.lines = [...chat, ...composer];
tui.addChild(component);
tui.renderNow();
await terminal.flush();

component.lines = [
...chat,
...Array.from({ length: 8 }, (_, index) => `Selector ${index}`),
];
tui.renderNow();
await terminal.flush();
component.lines = [...chat, ...composer];
tui.renderNow();
await terminal.flush();
terminal.takeWrites();

component.lines = [...chat.slice(0, 12), ...composer];
tui.renderNow();
await terminal.flush();

expect(terminal.takeWrites()).toContain('\x1b[3J');
expect(terminal.getViewport()).toEqual([
...chat.slice(5, 12),
'Editor 0',
'Editor 1',
'Editor 2',
]);
expect(terminal.getScrollBuffer()).toEqual([
...chat.slice(0, 12),
'Editor 0',
'Editor 1',
'Editor 2',
]);
expect(terminal.getCursorPosition()).toEqual({ x: 8, y: 9 });
});

it('does not replay scrolled answer rows after a narrow terminal frame shrinks', async () => {
const terminal = new RecordingVirtualTerminal(67, 44);
const tui = new TuiMainScreen(terminal);
Expand All @@ -99,11 +141,13 @@ describe('MCode Pi Engine local deltas', () => {
await terminal.flush();

// Settle the activity row, then repaint a historical line (for example Markdown styling).
terminal.takeWrites();
component.lines = [...answer, `composer${CURSOR_MARKER}`, 'status'];
component.lines[0] = '\x1b[1mAnswer line 0\x1b[0m';
tui.renderNow();
await terminal.flush();
terminal.takeWrites();
component.lines[0] = '\x1b[1mAnswer line 0\x1b[0m';
expect(terminal.takeWrites()).not.toContain('\x1b[3J');
component.lines[0] = '\x1b[3mAnswer line 0\x1b[0m';
tui.renderNow();
await terminal.flush();

Expand Down
Loading