Restore terminal input bar with explicit direct input mode

This commit is contained in:
sladro 2026-03-31 18:26:20 +08:00
parent d9cf4e72e2
commit 355e7b677b
3 changed files with 176 additions and 135 deletions

View File

@ -48,13 +48,17 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
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<TerminalPage> {
@override
void dispose() {
_terminalFocusNode.dispose();
_inputFocusNode.dispose();
_inputController.dispose();
_terminalScrollController.dispose();
unawaited(_coordinator.close());
controller.dispose();
@ -145,6 +151,20 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
);
}
Future<void> _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<TerminalPage> {
}) {
_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<void> _showDiagnostics() {
@ -577,7 +616,7 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
child: TerminalView(
terminal,
focusNode: _terminalFocusNode,
autofocus: true,
autofocus: false,
scrollController: _terminalScrollController,
),
),
@ -589,7 +628,7 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
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<TerminalPage> {
),
],
),
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,
),
),
],
),

View File

@ -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,

View File

@ -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