fix(terminal): fix new tab auto-focus and NaN error on data before layout (#14357)

- Fix new tab not auto-focusing: add FocusNode to TerminalView and
  request focus when tab is selected via tab state listener
- Fix NaN error when data arrives before terminal view layout: buffer
  output data until terminal view has valid dimensions, flush on first
  valid resize callback

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou
2026-02-20 14:44:25 +08:00
committed by GitHub
parent 34ceeac36e
commit 483fe80308
3 changed files with 120 additions and 6 deletions

View File

@@ -24,6 +24,13 @@ class TerminalModel with ChangeNotifier {
bool _disposed = false;
final _inputBuffer = <String>[];
// Buffer for output data received before terminal view has valid dimensions.
// This prevents NaN errors when writing to terminal before layout is complete.
final _pendingOutputChunks = <String>[];
int _pendingOutputSize = 0;
static const int _kMaxOutputBufferChars = 8 * 1024;
// View ready state: true when terminal has valid dimensions, safe to write
bool _terminalViewReady = false;
bool get isPeerWindows => parent.ffiModel.pi.platform == kPeerPlatformWindows;
@@ -74,6 +81,12 @@ class TerminalModel with ChangeNotifier {
// This piece of code must be placed before the conditional check in order to initialize properly.
onResizeExternal?.call(w, h, pw, ph);
// Mark terminal view as ready and flush any buffered output on first valid resize.
// Must be after onResizeExternal so the view layer has valid dimensions before flushing.
if (!_terminalViewReady) {
_markViewReady();
}
if (_terminalOpened) {
// Notify remote terminal of resize
try {
@@ -141,7 +154,7 @@ class TerminalModel with ChangeNotifier {
debugPrint('[TerminalModel] Error calling sessionOpenTerminal: $e');
// Optionally show error to user
if (e is TimeoutException) {
terminal.write('Failed to open terminal: Connection timeout\r\n');
_writeToTerminal('Failed to open terminal: Connection timeout\r\n');
}
}
}
@@ -283,7 +296,7 @@ class TerminalModel with ChangeNotifier {
}));
}
} else {
terminal.write('Failed to open terminal: $message\r\n');
_writeToTerminal('Failed to open terminal: $message\r\n');
}
}
@@ -327,29 +340,83 @@ class TerminalModel with ChangeNotifier {
return;
}
terminal.write(text);
_writeToTerminal(text);
} catch (e) {
debugPrint('[TerminalModel] Failed to process terminal data: $e');
}
}
}
/// Write text to terminal, buffering if the view is not yet ready.
/// All terminal output should go through this method to avoid NaN errors
/// from writing before the terminal view has valid layout dimensions.
void _writeToTerminal(String text) {
if (!_terminalViewReady) {
// If a single chunk exceeds the cap, keep only its tail.
// Note: truncation may split a multi-byte ANSI escape sequence,
// which can cause a brief visual glitch on flush. This is acceptable
// because it only affects the pre-layout buffering window and the
// terminal will self-correct on subsequent output.
if (text.length >= _kMaxOutputBufferChars) {
final truncated =
text.substring(text.length - _kMaxOutputBufferChars);
_pendingOutputChunks
..clear()
..add(truncated);
_pendingOutputSize = truncated.length;
} else {
_pendingOutputChunks.add(text);
_pendingOutputSize += text.length;
// Drop oldest chunks if exceeds limit (whole chunks to preserve ANSI sequences)
while (_pendingOutputSize > _kMaxOutputBufferChars &&
_pendingOutputChunks.length > 1) {
final removed = _pendingOutputChunks.removeAt(0);
_pendingOutputSize -= removed.length;
}
}
return;
}
terminal.write(text);
}
void _flushOutputBuffer() {
if (_pendingOutputChunks.isEmpty) return;
debugPrint(
'[TerminalModel] Flushing $_pendingOutputSize buffered chars (${_pendingOutputChunks.length} chunks)');
for (final chunk in _pendingOutputChunks) {
terminal.write(chunk);
}
_pendingOutputChunks.clear();
_pendingOutputSize = 0;
}
/// Mark terminal view as ready and flush buffered output.
void _markViewReady() {
if (_terminalViewReady) return;
_terminalViewReady = true;
_flushOutputBuffer();
}
void _handleTerminalClosed(Map<String, dynamic> evt) {
final int exitCode = evt['exit_code'] ?? 0;
terminal.write('\r\nTerminal closed with exit code: $exitCode\r\n');
_writeToTerminal('\r\nTerminal closed with exit code: $exitCode\r\n');
_terminalOpened = false;
notifyListeners();
}
void _handleTerminalError(Map<String, dynamic> evt) {
final String message = evt['message'] ?? 'Unknown error';
terminal.write('\r\nTerminal error: $message\r\n');
_writeToTerminal('\r\nTerminal error: $message\r\n');
}
@override
void dispose() {
if (_disposed) return;
_disposed = true;
// Clear buffers to free memory
_inputBuffer.clear();
_pendingOutputChunks.clear();
_pendingOutputSize = 0;
// Terminal cleanup is handled server-side when service closes
super.dispose();
}