fix terminal input ids across page re-entry

This commit is contained in:
sladro 2026-04-11 08:37:17 +08:00
parent 3d06ee0a19
commit eb98d9ae8d
2 changed files with 67 additions and 6 deletions

View File

@ -31,6 +31,8 @@ class TerminalViewport {
} }
class TerminalSessionCoordinator extends ChangeNotifier { class TerminalSessionCoordinator extends ChangeNotifier {
static int _inputDispatchInstanceCounter = 0;
TerminalSessionCoordinator({ TerminalSessionCoordinator({
required this.controller, required this.controller,
required this.apiClient, required this.apiClient,
@ -46,7 +48,8 @@ class TerminalSessionCoordinator extends ChangeNotifier {
ResizeScheduler? resizeScheduler, ResizeScheduler? resizeScheduler,
}) : baseUri = baseUri ?? _defaultBaseUri, }) : baseUri = baseUri ?? _defaultBaseUri,
_reconnectScheduler = reconnectScheduler ?? _defaultReconnectScheduler, _reconnectScheduler = reconnectScheduler ?? _defaultReconnectScheduler,
_resizeScheduler = resizeScheduler ?? _defaultResizeScheduler; _resizeScheduler = resizeScheduler ?? _defaultResizeScheduler,
_inputDispatchScope = _buildInputDispatchScope(session.sessionId);
static final Uri _defaultBaseUri = Uri( static final Uri _defaultBaseUri = Uri(
scheme: 'https', scheme: 'https',
@ -71,6 +74,7 @@ class TerminalSessionCoordinator extends ChangeNotifier {
final TerminalDiagnosticLog? diagnosticLog; final TerminalDiagnosticLog? diagnosticLog;
final ReconnectScheduler _reconnectScheduler; final ReconnectScheduler _reconnectScheduler;
final ResizeScheduler _resizeScheduler; final ResizeScheduler _resizeScheduler;
final String _inputDispatchScope;
TerminalSocketSession? _socketSession; TerminalSocketSession? _socketSession;
CancelReconnect? _cancelReconnect; CancelReconnect? _cancelReconnect;
@ -797,7 +801,12 @@ class TerminalSessionCoordinator extends ChangeNotifier {
String _buildPendingInputId() { String _buildPendingInputId() {
_nextInputId += 1; _nextInputId += 1;
return '${session.sessionId}-input-$_nextInputId'; return '$_inputDispatchScope-input-$_nextInputId';
}
static String _buildInputDispatchScope(String sessionId) {
_inputDispatchInstanceCounter += 1;
return '$sessionId-${_inputDispatchInstanceCounter}';
} }
} }

View File

@ -123,7 +123,11 @@ void main() {
expect(sessionFactory.createdSessions, hasLength(2)); expect(sessionFactory.createdSessions, hasLength(2));
expect(sessionFactory.createdSessions.last.sentInputs, ['dir\r']); 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; final firstSession = sessionFactory.createdSessions.single;
coordinator.sendInput('dir\r'); coordinator.sendInput('dir\r');
expect(firstSession.sentInputIds, ['abc-input-1']); final firstInputId = firstSession.sentInputIds.single;
expect(firstInputId, endsWith('-input-1'));
firstSession.disconnect(); firstSession.disconnect();
await reconnectScheduler.runPending(); await reconnectScheduler.runPending();
final secondSession = sessionFactory.createdSessions.last; final secondSession = sessionFactory.createdSessions.last;
expect(secondSession.sentInputs, ['dir\r']); 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(); secondSession.disconnect();
await reconnectScheduler.runPending(); 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', () { test('restore payloads are treated as authoritative over provisional text', () {
final decision = decideTerminalRestore( final decision = decideTerminalRestore(
currentText: 'PS> git status\r\nmodified: file.txt\r\nPS> ', currentText: 'PS> git status\r\nmodified: file.txt\r\nPS> ',