Harden terminal lifecycle reconnect
This commit is contained in:
parent
e1d01d539c
commit
f2282b8619
@ -61,6 +61,7 @@ class _TerminalPageState extends ConsumerState<TerminalPage>
|
||||
String? _pendingHistorySeed;
|
||||
bool _receivedSocketFrame = false;
|
||||
bool _historySeeded = false;
|
||||
bool _shouldReconnectOnResume = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -116,12 +117,21 @@ class _TerminalPageState extends ConsumerState<TerminalPage>
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state != AppLifecycleState.resumed) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
if (_shouldReconnectOnResume) {
|
||||
_shouldReconnectOnResume = false;
|
||||
_diagnosticLog.add('app.lifecycle.resumed', widget.session.sessionId);
|
||||
unawaited(_coordinator.reconnectNow());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
_diagnosticLog.add('app.lifecycle.resumed', widget.session.sessionId);
|
||||
unawaited(_coordinator.reconnectNow());
|
||||
if (state == AppLifecycleState.hidden ||
|
||||
state == AppLifecycleState.paused) {
|
||||
_shouldReconnectOnResume = true;
|
||||
_diagnosticLog.add('app.lifecycle.suspended', state.name);
|
||||
unawaited(_coordinator.suspendForBackground());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _openSiblingTerminal() async {
|
||||
|
||||
@ -195,6 +195,20 @@ class TerminalSessionCoordinator extends ChangeNotifier {
|
||||
await start(isReconnect: true);
|
||||
}
|
||||
|
||||
Future<void> suspendForBackground() async {
|
||||
_cancelPendingReconnect();
|
||||
|
||||
if (_isDisposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
controller.markDisconnected();
|
||||
_connectionStatus = 'App inactive. Reconnecting when active...';
|
||||
diagnosticLog?.add('socket.suspend', session.sessionId);
|
||||
notifyListeners();
|
||||
await _closeActiveSession();
|
||||
}
|
||||
|
||||
Future<void> close() async {
|
||||
_isDisposed = true;
|
||||
_cancelPendingReconnect();
|
||||
|
||||
@ -125,6 +125,33 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
test('suspendForBackground closes the active socket session', () async {
|
||||
final controller = TerminalInteractionController();
|
||||
final apiClient = _FakeAgentApiClient();
|
||||
final sessionFactory = _FakeTerminalSessionFactory();
|
||||
final session = Session(
|
||||
sessionId: 'abc',
|
||||
name: 'codex-main',
|
||||
status: 'idle',
|
||||
);
|
||||
final coordinator = TerminalSessionCoordinator(
|
||||
controller: controller,
|
||||
apiClient: apiClient,
|
||||
session: session,
|
||||
sessionFactory: sessionFactory.create,
|
||||
onFrame: (_) {},
|
||||
viewportProvider: () => const TerminalViewport(columns: 80, rows: 24),
|
||||
);
|
||||
|
||||
await coordinator.start();
|
||||
final socketSession = sessionFactory.createdSessions.single;
|
||||
|
||||
await coordinator.suspendForBackground();
|
||||
|
||||
expect(socketSession.disposeCount, 1);
|
||||
expect(controller.connectionState, TerminalConnectionState.disconnected);
|
||||
});
|
||||
|
||||
test(
|
||||
'incoming frames while browsing history flag pending live output',
|
||||
() async {
|
||||
@ -224,6 +251,7 @@ class _FakeTerminalSocketSession extends TerminalSocketSession {
|
||||
|
||||
final bool autoConnect;
|
||||
final resizeCalls = <List<int>>[];
|
||||
int disposeCount = 0;
|
||||
Completer<void> _connectCompleter = Completer<void>();
|
||||
void Function(String frame)? _onFrame;
|
||||
void Function()? _onDisconnected;
|
||||
@ -259,6 +287,11 @@ class _FakeTerminalSocketSession extends TerminalSocketSession {
|
||||
void disconnect() {
|
||||
_onDisconnected?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
disposeCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
class _FakeAgentSocketClient extends AgentSocketClient {
|
||||
|
||||
@ -608,6 +608,36 @@ void main() {
|
||||
expect(find.text('codex-main'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'terminal page does not reconnect after an inactive-only interruption',
|
||||
(tester) async {
|
||||
final transportFactory = _QueuedTerminalSocketTransportFactory();
|
||||
|
||||
await _pumpApp(
|
||||
tester,
|
||||
projectRepository: _FakeProjectRepository(),
|
||||
sessionRepository: _FakeSessionRepository(),
|
||||
socketFactory: TerminalSocketSessionFactory(
|
||||
transportFactory: transportFactory.create,
|
||||
),
|
||||
);
|
||||
|
||||
await _openProjectTerminal(tester);
|
||||
|
||||
expect(transportFactory.createCount, 1);
|
||||
|
||||
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive);
|
||||
await tester.pump();
|
||||
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 2));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(transportFactory.createCount, 1);
|
||||
expect(find.text('codex-main'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('terminal page can open another terminal for the same project', (
|
||||
tester,
|
||||
) async {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user