From 7b401eb3f69fb84f24bb6638778493db9ee5d511 Mon Sep 17 00:00:00 2001 From: sladro Date: Fri, 10 Apr 2026 21:22:07 +0800 Subject: [PATCH] Expand terminal recovery to multi-page recent history --- .../terminal_session_coordinator.dart | 24 ++++++- .../terminal_session_coordinator_test.dart | 71 +++++++++++++++++++ apps/mobile_app/test/widget_test.dart | 27 +------ 3 files changed, 97 insertions(+), 25 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 403892e..2d56134 100644 --- a/apps/mobile_app/lib/features/terminal/terminal_session_coordinator.dart +++ b/apps/mobile_app/lib/features/terminal/terminal_session_coordinator.dart @@ -56,6 +56,7 @@ class TerminalSessionCoordinator extends ChangeNotifier { static const Duration reconnectDelay = Duration(seconds: 1); static const Duration resizeDebounceDelay = Duration(milliseconds: 120); static const int historyPageSize = 200; + static const int recentRecoveryOutputLineTarget = 1000; static const int pendingInputCharacterLimit = 2048; final TerminalInteractionController controller; @@ -361,7 +362,15 @@ class TerminalSessionCoordinator extends ChangeNotifier { } Future loadRecentHistoryWindow() async { - return _loadHistory(); + var history = controller.historyWindow.outputSeedText.isNotEmpty + ? controller.historyWindow + : await _loadHistory(); + while (history != null && + history.hasMoreAbove && + _countOutputLines(history.outputSeedText) < recentRecoveryOutputLineTarget) { + history = await _loadHistory(loadOlder: true); + } + return history; } Future _loadHistory({bool loadOlder = false}) async { @@ -682,6 +691,19 @@ class TerminalSessionCoordinator extends ChangeNotifier { return buffer.toString(); } + int _countOutputLines(String text) { + if (text.isEmpty) { + return 0; + } + + final normalized = text.replaceAll('\r\n', '\n').replaceAll('\r', '\n'); + final parts = normalized.split('\n'); + if (parts.isNotEmpty && parts.last.isEmpty) { + return parts.length - 1; + } + return parts.length; + } + List<_JournalItem> _readJournalItems(Map payload) { final rawItems = (payload['items'] as List?) ?? const []; return rawItems 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 b27cc42..887c7d4 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 @@ -713,6 +713,77 @@ void main() { expect(receivedFrames, ['gap-5', 'gap-6']); }, ); + + test( + 'loadRecentHistoryWindow pages backward until the recent recovery window is filled', + () async { + final controller = TerminalInteractionController(); + controller.applyFrame('skip-initial-history-load'); + final apiClient = _FakeAgentApiClient( + journalResponses: [ + { + 'sessionId': 'abc', + 'items': [ + { + 'sessionId': 'abc', + 'sequence': 6, + 'kind': 'output', + 'payload': 'middle\r\n', + 'timestampUtc': '2026-04-07T03:20:06Z', + }, + { + 'sessionId': 'abc', + 'sequence': 7, + 'kind': 'output', + 'payload': 'tail', + 'timestampUtc': '2026-04-07T03:20:07Z', + }, + ], + 'hasMoreBefore': true, + 'hasMoreAfter': false, + 'currentSequence': 7, + }, + { + 'sessionId': 'abc', + 'items': [ + { + 'sessionId': 'abc', + 'sequence': 5, + 'kind': 'output', + 'payload': 'header\r\n', + 'timestampUtc': '2026-04-07T03:20:05Z', + }, + ], + 'hasMoreBefore': false, + 'hasMoreAfter': true, + 'currentSequence': 7, + }, + ], + ); + 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: (_) {}, + onRestore: (_) {}, + viewportProvider: () => const TerminalViewport(columns: 80, rows: 24), + ); + + await coordinator.start(); + final history = await coordinator.loadRecentHistoryWindow(); + + expect(apiClient.requestedJournalBeforeSequences, [null, 6]); + expect(history, isNotNull); + expect(history!.outputSeedText, 'header\r\nmiddle\r\ntail'); + }, + ); } class _FakeAgentApiClient extends AgentApiClient { diff --git a/apps/mobile_app/test/widget_test.dart b/apps/mobile_app/test/widget_test.dart index 139d385..e4f4f5a 100644 --- a/apps/mobile_app/test/widget_test.dart +++ b/apps/mobile_app/test/widget_test.dart @@ -1258,13 +1258,6 @@ void main() { { 'sessionId': 'session-1', 'items': [ - { - 'sessionId': 'session-1', - 'sequence': 5, - 'kind': 'output', - 'payload': 'header\r\n', - 'timestampUtc': '2026-04-07T03:20:05Z', - }, { 'sessionId': 'session-1', 'sequence': 6, @@ -1280,7 +1273,7 @@ void main() { 'timestampUtc': '2026-04-07T03:20:07Z', }, ], - 'hasMoreBefore': false, + 'hasMoreBefore': true, 'hasMoreAfter': false, 'currentSequence': 8, }, @@ -1294,23 +1287,9 @@ void main() { 'payload': 'header\r\n', 'timestampUtc': '2026-04-07T03:20:05Z', }, - { - 'sessionId': 'session-1', - 'sequence': 6, - 'kind': 'output', - 'payload': 'middle-output\r\n', - 'timestampUtc': '2026-04-07T03:20:06Z', - }, - { - 'sessionId': 'session-1', - 'sequence': 7, - 'kind': 'output', - 'payload': 'tail-only', - 'timestampUtc': '2026-04-07T03:20:07Z', - }, ], 'hasMoreBefore': false, - 'hasMoreAfter': false, + 'hasMoreAfter': true, 'currentSequence': 8, }, ], @@ -1342,7 +1321,7 @@ void main() { expect(terminal.buffer.getText(), contains('header')); expect(terminal.buffer.getText(), contains('middle-output')); - expect(apiClient.requestedBeforeSequences, [null, null]); + expect(apiClient.requestedBeforeSequences, [null, 6]); }, );