diff --git a/apps/mobile_app/lib/features/terminal/terminal_page.dart b/apps/mobile_app/lib/features/terminal/terminal_page.dart index cd17f40..3194ef4 100644 --- a/apps/mobile_app/lib/features/terminal/terminal_page.dart +++ b/apps/mobile_app/lib/features/terminal/terminal_page.dart @@ -46,6 +46,7 @@ class TerminalPage extends ConsumerStatefulWidget { class _TerminalPageState extends ConsumerState with WidgetsBindingObserver { static const Duration _historySeedDelay = Duration(milliseconds: 120); + static const Duration _terminalResizeSettleDelay = Duration(milliseconds: 240); static const List<_QuickTerminalKey> _editingControlKeys = [ _QuickTerminalKey(keyId: 'esc', label: 'Esc', input: '\u001b'), _QuickTerminalKey(keyId: 'tab', label: 'Tab', input: '\t'), @@ -147,6 +148,7 @@ class _TerminalPageState extends ConsumerState late final TerminalSnapshotStorage _snapshotStorage; late final Listenable _pageStateListenable; Timer? _historySeedTimer; + Timer? _terminalResizeSettleTimer; Timer? _snapshotPersistTimer; String? _pendingHistorySeed; bool _receivedSocketFrame = false; @@ -156,6 +158,7 @@ class _TerminalPageState extends ConsumerState bool _awaitingReconnectRestore = false; bool _shouldReconnectOnResume = false; bool _showExpandedControls = false; + bool _terminalAutoResizeEnabled = true; _TerminalInputMode _inputMode = _TerminalInputMode.read; TerminalConnectionState? _lastConnectionState; @@ -189,7 +192,7 @@ class _TerminalPageState extends ConsumerState _diagnosticLog.add('ui.terminal.key', normalizedInput); _coordinator.sendInput(normalizedInput); }; - _applyInputMode(); + _applyInputMode(freezeResize: false); unawaited(_bootstrapTerminal()); } @@ -198,6 +201,7 @@ class _TerminalPageState extends ConsumerState WidgetsBinding.instance.removeObserver(this); _pageStateListenable.removeListener(_handlePageStateChanged); _historySeedTimer?.cancel(); + _terminalResizeSettleTimer?.cancel(); _snapshotPersistTimer?.cancel(); unawaited(_persistTerminalSnapshot()); _terminalFocusNode.dispose(); @@ -226,6 +230,17 @@ class _TerminalPageState extends ConsumerState } } + @override + void didChangeMetrics() { + if (_inputMode == _TerminalInputMode.edit) { + _cancelTerminalResizeSettle(); + _setTerminalAutoResizeEnabled(false); + return; + } + + _freezeTerminalResizeUntilSettled(); + } + Future _openSiblingTerminal() async { final project = widget.project; if (project == null) { @@ -329,8 +344,17 @@ class _TerminalPageState extends ConsumerState _applyInputMode(); } - void _applyInputMode() { + void _applyInputMode({bool freezeResize = true}) { _coordinator.setBackendResizeEnabled(_inputMode != _TerminalInputMode.edit); + if (_inputMode == _TerminalInputMode.edit) { + _cancelTerminalResizeSettle(); + _setTerminalAutoResizeEnabled(false); + } else if (freezeResize) { + _freezeTerminalResizeUntilSettled(); + } else { + _cancelTerminalResizeSettle(); + _setTerminalAutoResizeEnabled(true); + } _terminalFocusNode.canRequestFocus = _inputMode == _TerminalInputMode.edit; if (_inputMode == _TerminalInputMode.edit) { _terminalFocusNode.requestFocus(); @@ -340,6 +364,34 @@ class _TerminalPageState extends ConsumerState _terminalFocusNode.unfocus(); } + void _freezeTerminalResizeUntilSettled() { + _setTerminalAutoResizeEnabled(false); + _terminalResizeSettleTimer?.cancel(); + _terminalResizeSettleTimer = Timer(_terminalResizeSettleDelay, () { + _terminalResizeSettleTimer = null; + if (!mounted || _inputMode == _TerminalInputMode.edit) { + return; + } + + _setTerminalAutoResizeEnabled(true); + }); + } + + void _cancelTerminalResizeSettle() { + _terminalResizeSettleTimer?.cancel(); + _terminalResizeSettleTimer = null; + } + + void _setTerminalAutoResizeEnabled(bool enabled) { + if (_terminalAutoResizeEnabled == enabled || !mounted) { + return; + } + + setState(() { + _terminalAutoResizeEnabled = enabled; + }); + } + void _handleTerminalSurfaceTap() { if (_inputMode != _TerminalInputMode.edit) { return; @@ -721,7 +773,7 @@ class _TerminalPageState extends ConsumerState controller: _terminalViewController, focusNode: _terminalFocusNode, autofocus: false, - autoResize: false, + autoResize: _terminalAutoResizeEnabled, keyboardType: TextInputType.multiline, deleteDetection: true, readOnly: _inputMode == _TerminalInputMode.read, diff --git a/apps/mobile_app/test/features/terminal/terminal_page_input_test.dart b/apps/mobile_app/test/features/terminal/terminal_page_input_test.dart index 5b90b12..aca5a56 100644 --- a/apps/mobile_app/test/features/terminal/terminal_page_input_test.dart +++ b/apps/mobile_app/test/features/terminal/terminal_page_input_test.dart @@ -72,13 +72,30 @@ void main() { expect(terminalView.deleteDetection, isTrue); }); - testWidgets('terminal view disables auto resize and text reflow', (tester) async { + testWidgets('terminal view freezes auto resize while editing and restores it after returning to read', (tester) async { await _pumpTerminalPage(tester); - final terminalView = tester.widget(find.byType(TerminalView)); + var terminalView = tester.widget(find.byType(TerminalView)); - expect(terminalView.autoResize, isFalse); + expect(terminalView.autoResize, isTrue); expect(terminalView.terminal.reflowEnabled, isFalse); + + await tester.tap(find.byKey(const Key('terminal_mode_edit_button'))); + await tester.pump(); + + terminalView = tester.widget(find.byType(TerminalView)); + expect(terminalView.autoResize, isFalse); + + await tester.tap(find.byKey(const Key('terminal_mode_read_button'))); + await tester.pump(); + + terminalView = tester.widget(find.byType(TerminalView)); + expect(terminalView.autoResize, isFalse); + + await tester.pump(const Duration(milliseconds: 260)); + + terminalView = tester.widget(find.byType(TerminalView)); + expect(terminalView.autoResize, isTrue); }); testWidgets(