From eb98d9ae8dac00c6a95484a4a3b7dbc384d19cb0 Mon Sep 17 00:00:00 2001 From: sladro Date: Sat, 11 Apr 2026 08:37:17 +0800 Subject: [PATCH] fix terminal input ids across page re-entry --- .../terminal_session_coordinator.dart | 13 +++- .../terminal_session_coordinator_test.dart | 60 +++++++++++++++++-- 2 files changed, 67 insertions(+), 6 deletions(-) diff --git a/apps/mobile_app/lib/features/terminal/terminal_session_coordinator.dart b/apps/mobile_app/lib/features/terminal/terminal_session_coordinator.dart index eeaf828..e8de806 100644 --- a/apps/mobile_app/lib/features/terminal/terminal_session_coordinator.dart +++ b/apps/mobile_app/lib/features/terminal/terminal_session_coordinator.dart @@ -31,6 +31,8 @@ class TerminalViewport { } class TerminalSessionCoordinator extends ChangeNotifier { + static int _inputDispatchInstanceCounter = 0; + TerminalSessionCoordinator({ required this.controller, required this.apiClient, @@ -46,7 +48,8 @@ class TerminalSessionCoordinator extends ChangeNotifier { ResizeScheduler? resizeScheduler, }) : baseUri = baseUri ?? _defaultBaseUri, _reconnectScheduler = reconnectScheduler ?? _defaultReconnectScheduler, - _resizeScheduler = resizeScheduler ?? _defaultResizeScheduler; + _resizeScheduler = resizeScheduler ?? _defaultResizeScheduler, + _inputDispatchScope = _buildInputDispatchScope(session.sessionId); static final Uri _defaultBaseUri = Uri( scheme: 'https', @@ -71,6 +74,7 @@ class TerminalSessionCoordinator extends ChangeNotifier { final TerminalDiagnosticLog? diagnosticLog; final ReconnectScheduler _reconnectScheduler; final ResizeScheduler _resizeScheduler; + final String _inputDispatchScope; TerminalSocketSession? _socketSession; CancelReconnect? _cancelReconnect; @@ -797,7 +801,12 @@ class TerminalSessionCoordinator extends ChangeNotifier { String _buildPendingInputId() { _nextInputId += 1; - return '${session.sessionId}-input-$_nextInputId'; + return '$_inputDispatchScope-input-$_nextInputId'; + } + + static String _buildInputDispatchScope(String sessionId) { + _inputDispatchInstanceCounter += 1; + return '$sessionId-${_inputDispatchInstanceCounter}'; } } diff --git a/apps/mobile_app/test/features/terminal/terminal_session_coordinator_test.dart b/apps/mobile_app/test/features/terminal/terminal_session_coordinator_test.dart index 2c088c2..3e32191 100644 --- a/apps/mobile_app/test/features/terminal/terminal_session_coordinator_test.dart +++ b/apps/mobile_app/test/features/terminal/terminal_session_coordinator_test.dart @@ -123,7 +123,11 @@ void main() { expect(sessionFactory.createdSessions, hasLength(2)); expect(sessionFactory.createdSessions.last.sentInputs, ['dir\r']); - expect(sessionFactory.createdSessions.last.sentInputIds, ['abc-input-1']); + expect(sessionFactory.createdSessions.last.sentInputIds, hasLength(1)); + expect( + sessionFactory.createdSessions.last.sentInputIds.single, + endsWith('-input-1'), + ); }, ); @@ -154,16 +158,17 @@ void main() { final firstSession = sessionFactory.createdSessions.single; coordinator.sendInput('dir\r'); - expect(firstSession.sentInputIds, ['abc-input-1']); + final firstInputId = firstSession.sentInputIds.single; + expect(firstInputId, endsWith('-input-1')); firstSession.disconnect(); await reconnectScheduler.runPending(); final secondSession = sessionFactory.createdSessions.last; expect(secondSession.sentInputs, ['dir\r']); - expect(secondSession.sentInputIds, ['abc-input-1']); + expect(secondSession.sentInputIds, [firstInputId]); - secondSession.ackInput('abc-input-1'); + secondSession.ackInput(firstInputId); secondSession.disconnect(); await reconnectScheduler.runPending(); @@ -172,6 +177,53 @@ void main() { }, ); + test('re-entering the same session generates fresh input ids', () async { + final firstController = TerminalInteractionController(); + final firstApiClient = _FakeAgentApiClient(); + final firstSessionFactory = _FakeTerminalSessionFactory(); + final session = Session( + sessionId: 'abc', + name: 'codex-main', + status: 'idle', + ); + final firstCoordinator = TerminalSessionCoordinator( + controller: firstController, + apiClient: firstApiClient, + session: session, + sessionFactory: firstSessionFactory.create, + onFrame: (_) {}, + onRestore: (_) {}, + viewportProvider: () => const TerminalViewport(columns: 80, rows: 24), + ); + + await firstCoordinator.start(); + firstCoordinator.sendInput('dir\r'); + final firstInputId = firstSessionFactory.createdSessions.single.sentInputIds.single; + await firstCoordinator.close(); + + final secondController = TerminalInteractionController(); + final secondApiClient = _FakeAgentApiClient(); + final secondSessionFactory = _FakeTerminalSessionFactory(); + final secondCoordinator = TerminalSessionCoordinator( + controller: secondController, + apiClient: secondApiClient, + session: session, + sessionFactory: secondSessionFactory.create, + onFrame: (_) {}, + onRestore: (_) {}, + viewportProvider: () => const TerminalViewport(columns: 80, rows: 24), + ); + + await secondCoordinator.start(); + secondCoordinator.sendInput('dir\r'); + final secondInputId = secondSessionFactory.createdSessions.single.sentInputIds.single; + + expect(secondInputId, isNot(firstInputId)); + expect(firstInputId, endsWith('-input-1')); + expect(secondInputId, endsWith('-input-1')); + await secondCoordinator.close(); + }); + test('restore payloads are treated as authoritative over provisional text', () { final decision = decideTerminalRestore( currentText: 'PS> git status\r\nmodified: file.txt\r\nPS> ',