diff --git a/apps/mobile_app/lib/features/terminal/terminal_page.dart b/apps/mobile_app/lib/features/terminal/terminal_page.dart index e2f88ed..f1b3464 100644 --- a/apps/mobile_app/lib/features/terminal/terminal_page.dart +++ b/apps/mobile_app/lib/features/terminal/terminal_page.dart @@ -48,13 +48,17 @@ class _TerminalPageState extends ConsumerState { TerminalInteractionController(); final TerminalDiagnosticLog _diagnosticLog = TerminalDiagnosticLog(); final FocusNode _terminalFocusNode = FocusNode(); + final FocusNode _inputFocusNode = FocusNode(); + final TextEditingController _inputController = TextEditingController(); final ScrollController _terminalScrollController = ScrollController(); late final TerminalSessionCoordinator _coordinator; late final Listenable _controllerAndCoordinator; + bool _isDirectInputEnabled = false; @override void initState() { super.initState(); + _terminalFocusNode.canRequestFocus = false; _coordinator = TerminalSessionCoordinator( controller: controller, apiClient: ref.read(agentApiClientProvider), @@ -87,6 +91,8 @@ class _TerminalPageState extends ConsumerState { @override void dispose() { _terminalFocusNode.dispose(); + _inputFocusNode.dispose(); + _inputController.dispose(); _terminalScrollController.dispose(); unawaited(_coordinator.close()); controller.dispose(); @@ -145,6 +151,20 @@ class _TerminalPageState extends ConsumerState { ); } + Future _sendLine() async { + final input = _inputController.text; + if (!_canSendInput || input.trim().isEmpty) { + return; + } + + _sendTerminalInput( + '$input\r', + diagnosticEvent: 'ui.input.send', + detail: input, + ); + _inputController.clear(); + } + void _sendQuickKey(_QuickTerminalKey quickKey) { _sendTerminalInput( quickKey.input, @@ -160,7 +180,26 @@ class _TerminalPageState extends ConsumerState { }) { _diagnosticLog.add(diagnosticEvent, detail); _coordinator.sendInput(input); - _terminalFocusNode.requestFocus(); + if (_isDirectInputEnabled) { + _terminalFocusNode.requestFocus(); + } else { + _inputFocusNode.requestFocus(); + } + } + + void _toggleDirectInput() { + final enabled = !_isDirectInputEnabled; + setState(() { + _isDirectInputEnabled = enabled; + _terminalFocusNode.canRequestFocus = enabled; + }); + + if (enabled) { + _terminalFocusNode.requestFocus(); + } else { + _terminalFocusNode.unfocus(); + _inputFocusNode.requestFocus(); + } } Future _showDiagnostics() { @@ -577,7 +616,7 @@ class _TerminalPageState extends ConsumerState { child: TerminalView( terminal, focusNode: _terminalFocusNode, - autofocus: true, + autofocus: false, scrollController: _terminalScrollController, ), ), @@ -589,7 +628,7 @@ class _TerminalPageState extends ConsumerState { animation: _controllerAndCoordinator, builder: (context, _) { return Container( - key: const Key('terminal_action_bar'), + key: const Key('terminal_input_bar'), padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 8, @@ -610,37 +649,97 @@ class _TerminalPageState extends ConsumerState { ), ], ), - child: Row( + child: Column( + key: const Key('terminal_action_bar'), + mainAxisSize: MainAxisSize.min, children: [ - Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: _quickTerminalKeys - .map((quickKey) { - return Padding( - padding: const EdgeInsets.only(right: 8), - child: OutlinedButton( - key: Key( - 'terminal_quick_key_${quickKey.keyId}', - ), - onPressed: _canSendInput - ? () => _sendQuickKey(quickKey) - : null, - child: Text(quickKey.label), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: _quickTerminalKeys + .map((quickKey) { + return Padding( + padding: const EdgeInsets.only(right: 8), + child: OutlinedButton( + key: Key( + 'terminal_quick_key_${quickKey.keyId}', ), - ); - }) - .toList(growable: false), - ), + onPressed: _canSendInput + ? () => _sendQuickKey(quickKey) + : null, + child: Text(quickKey.label), + ), + ); + }) + .toList(growable: false), ), ), - const SizedBox(width: 8), - IconButton.filledTonal( - key: const Key('terminal_toggle_actions_button'), - onPressed: _showToolsSheet, - icon: const Icon(Icons.tune), - tooltip: 'Show tools', + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: TextField( + controller: _inputController, + focusNode: _inputFocusNode, + enabled: _canSendInput, + decoration: const InputDecoration( + isDense: true, + hintText: 'Send input', + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric( + horizontal: 8, + vertical: 10, + ), + ), + onSubmitted: (_) => _sendLine(), + ), + ), + const SizedBox(width: 8), + FilledButton( + key: const Key('terminal_send_button'), + onPressed: _canSendInput ? _sendLine : null, + style: FilledButton.styleFrom( + minimumSize: const Size(0, 44), + padding: const EdgeInsets.symmetric( + horizontal: 14, + ), + ), + child: const Text('Send'), + ), + const SizedBox(width: 8), + IconButton.filledTonal( + key: const Key('terminal_direct_input_toggle'), + onPressed: _canSendInput + ? _toggleDirectInput + : null, + icon: Icon( + _isDirectInputEnabled + ? Icons.keyboard_hide + : Icons.keyboard, + ), + tooltip: _isDirectInputEnabled + ? 'Disable direct input' + : 'Enable direct input', + ), + const SizedBox(width: 8), + IconButton.filledTonal( + key: const Key('terminal_toggle_actions_button'), + onPressed: _showToolsSheet, + icon: const Icon(Icons.tune), + tooltip: 'Show tools', + ), + ], + ), + const SizedBox(height: 6), + Align( + alignment: Alignment.centerLeft, + child: Text( + _isDirectInputEnabled + ? 'Direct input on' + : 'Browse mode', + key: const Key('terminal_input_mode_label'), + style: Theme.of(context).textTheme.bodySmall, + ), ), ], ), diff --git a/apps/mobile_app/test/widget_test.dart b/apps/mobile_app/test/widget_test.dart index 10c2b96..a6eb95a 100644 --- a/apps/mobile_app/test/widget_test.dart +++ b/apps/mobile_app/test/widget_test.dart @@ -109,8 +109,9 @@ void main() { findsOneWidget, ); expect(find.byKey(const Key('terminal_action_bar')), findsOneWidget); - expect(find.byKey(const Key('terminal_send_button')), findsNothing); - expect(find.byKey(const Key('terminal_input_bar')), findsNothing); + expect(find.byKey(const Key('terminal_send_button')), findsOneWidget); + expect(find.byKey(const Key('terminal_input_bar')), findsOneWidget); + expect(find.text('Browse mode'), findsOneWidget); await tester.tap(find.byKey(const Key('terminal_toggle_actions_button'))); await tester.pumpAndSettle(); @@ -154,6 +155,32 @@ void main() { ); }); + testWidgets('terminal direct input mode is explicit and toggleable', ( + tester, + ) async { + await _pumpApp( + tester, + projectRepository: _FakeProjectRepository(), + sessionRepository: _FakeSessionRepository(), + ); + + await _openProjectTerminal(tester); + + expect(find.text('Browse mode'), findsOneWidget); + expect(find.text('Direct input on'), findsNothing); + + await tester.tap(find.byKey(const Key('terminal_direct_input_toggle'))); + await tester.pumpAndSettle(); + + expect(find.text('Direct input on'), findsOneWidget); + expect(find.text('Browse mode'), findsNothing); + + await tester.tap(find.byKey(const Key('terminal_direct_input_toggle'))); + await tester.pumpAndSettle(); + + expect(find.text('Browse mode'), findsOneWidget); + }); + testWidgets( 'project launch surfaces a friendly message when the working directory is invalid', (tester) async { @@ -190,6 +217,9 @@ void main() { ); await _openProjectTerminal(tester); + await tester.enterText(find.byType(TextField).last, 'dir'); + await tester.tap(find.byKey(const Key('terminal_send_button'))); + await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('terminal_quick_key_ctrl_l'))); await tester.pumpAndSettle(); @@ -201,7 +231,7 @@ void main() { await tester.pumpAndSettle(); expect(find.textContaining('ui.input.quick | Ctrl+L'), findsOneWidget); - expect(find.textContaining('socket.input.tx | '), findsOneWidget); + expect(find.textContaining('socket.input.tx | '), findsWidgets); expect( find.textContaining('socket.frame.rx | command-output'), findsOneWidget, diff --git a/work/windows_agent.stdout.log b/work/windows_agent.stdout.log index 87ad378..0046b9d 100644 --- a/work/windows_agent.stdout.log +++ b/work/windows_agent.stdout.log @@ -10,118 +10,30 @@ info: Microsoft.Hosting.Lifetime[0] info: Microsoft.Hosting.Lifetime[0] Content root path: D:\App\Flutter\TermRemoteCtl\apps\windows_agent\src\TermRemoteCtl.Agent info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=l + Terminal diagnostic backend.input.received session=84b9a32c2b0e4100aacdfbdb0026c7dc detail=d info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=s + Terminal diagnostic backend.input.received session=84b9a32c2b0e4100aacdfbdb0026c7dc detail=i info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=ls + Terminal diagnostic backend.input.received session=84b9a32c2b0e4100aacdfbdb0026c7dc detail=r info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail= + Terminal diagnostic backend.input.received session=84b9a32c2b0e4100aacdfbdb0026c7dc detail=dir info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail= + Terminal diagnostic backend.input.received session=84b9a32c2b0e4100aacdfbdb0026c7dc detail= info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=\r + Terminal diagnostic backend.input.received session=84b9a32c2b0e4100aacdfbdb0026c7dc detail= info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=d + Terminal diagnostic backend.input.received session=84b9a32c2b0e4100aacdfbdb0026c7dc detail= info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=i + Terminal diagnostic backend.input.received session=84b9a32c2b0e4100aacdfbdb0026c7dc detail= info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=r + Terminal diagnostic backend.input.received session=84b9a32c2b0e4100aacdfbdb0026c7dc detail=\r info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=dir + Terminal diagnostic backend.input.received session=84b9a32c2b0e4100aacdfbdb0026c7dc detail=d info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail= + Terminal diagnostic backend.input.received session=84b9a32c2b0e4100aacdfbdb0026c7dc detail=i info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail= + Terminal diagnostic backend.input.received session=84b9a32c2b0e4100aacdfbdb0026c7dc detail=r info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail= + Terminal diagnostic backend.input.received session=84b9a32c2b0e4100aacdfbdb0026c7dc detail=dir\r info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail= -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=r -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=r -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=\r -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=\r -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=l -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=s -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=ls -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=\r -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=ls\r -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=dir\r -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=pwd\r -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=c -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=o -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=d -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=e -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=x -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=codex -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail= -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail= -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail= -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail= -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail= -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=\r -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=这个项目是干什么的\r -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=有哪些具体功能?\r -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=6 -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail= -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=/ -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=e -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=x -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=i -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=t -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=exit -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail= -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail= -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail= -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail= -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail= -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=t -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail= -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=t -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=t -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail= -info: TermRemoteCtl.Agent.Terminal.LoggingTerminalDiagnosticsSink[0] - Terminal diagnostic backend.input.received session=023dbc621ca24fe0871f6bd38a701aea detail=\r + Terminal diagnostic backend.input.received session=84b9a32c2b0e4100aacdfbdb0026c7dc detail=dir\r