diff --git a/apps/mobile_app/lib/features/terminal/terminal_page.dart b/apps/mobile_app/lib/features/terminal/terminal_page.dart index ccc680e..26b1c4c 100644 --- a/apps/mobile_app/lib/features/terminal/terminal_page.dart +++ b/apps/mobile_app/lib/features/terminal/terminal_page.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:xterm/xterm.dart'; +import '../../app/app_theme.dart'; import '../../app/ui_shell.dart'; import '../../core/network/agent_connection_providers.dart'; import '../../core/network/agent_error_formatter.dart'; @@ -393,6 +394,7 @@ class _TerminalPageState extends ConsumerState Widget build(BuildContext context) { final width = MediaQuery.sizeOf(context).width; final isCompact = width < 420; + final isTight = width < 400; final workingDirectory = widget.project?.workingDirectory ?? widget.session.workingDirectory ?? @@ -415,12 +417,13 @@ class _TerminalPageState extends ConsumerState overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.titleSmall, ), - Text( - workingDirectory, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodySmall, - ), + if (!isTight) + Text( + workingDirectory, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall, + ), ], ), ), @@ -431,7 +434,12 @@ class _TerminalPageState extends ConsumerState final mode = controller.isFollowingLiveOutput ? 'Live' : 'Scrollback'; - final modeLabel = '$mode | ${controller.liveLines.length} lines'; + final modeLabel = isCompact + ? mode + : '$mode | ${controller.liveLines.length} lines'; + final statusLabel = isCompact + ? _compactStatusLabel + : _statusLabel; return Row( key: const Key('terminal_status_summary'), @@ -450,12 +458,12 @@ class _TerminalPageState extends ConsumerState ), child: Text(modeLabel), ), - const SizedBox(width: 4), + SizedBox(width: isCompact ? 2 : 4), Padding( - padding: const EdgeInsets.only(right: 8), + padding: EdgeInsets.only(right: isCompact ? 4 : 8), child: Center( child: StatusPill( - label: _statusLabel, + label: statusLabel, icon: _statusIcon, color: _statusColor(context), ), @@ -467,8 +475,9 @@ class _TerminalPageState extends ConsumerState ), ], ), - body: Padding( - padding: const EdgeInsets.fromLTRB(16, 6, 16, 10), + body: SafeArea( + top: false, + minimum: AppTheme.pagePadding, child: TextFieldTapRegion( child: Column( children: [ @@ -491,11 +500,17 @@ class _TerminalPageState extends ConsumerState borderRadius: BorderRadius.zero, border: Border.all(color: const Color(0xFF332B22)), ), - child: TerminalView( - terminal, - focusNode: _terminalFocusNode, - autofocus: false, - scrollController: _terminalScrollController, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 8, + ), + child: TerminalView( + terminal, + focusNode: _terminalFocusNode, + autofocus: false, + scrollController: _terminalScrollController, + ), ), ), ), @@ -975,6 +990,13 @@ class _TerminalPageState extends ConsumerState TerminalConnectionState.disconnected => 'Offline', }; + String get _compactStatusLabel => switch (_connectionState) { + TerminalConnectionState.connecting => 'Sync', + TerminalConnectionState.connected => 'On', + TerminalConnectionState.reconnecting => 'Sync', + TerminalConnectionState.disconnected => 'Off', + }; + bool get _canSendInput => controller.canSendInput; IconData get _statusIcon => switch (_connectionState) { diff --git a/apps/mobile_app/test/widget_test.dart b/apps/mobile_app/test/widget_test.dart index 3436166..2a86770 100644 --- a/apps/mobile_app/test/widget_test.dart +++ b/apps/mobile_app/test/widget_test.dart @@ -210,6 +210,52 @@ void main() { expect(find.text('Recent sessions'), findsOneWidget); }); + testWidgets( + 'terminal page keeps the command deck above the bottom safe area', + (tester) async { + tester.view.devicePixelRatio = 1; + tester.view.physicalSize = const Size(393, 852); + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + final mediaQueryData = MediaQueryData.fromView(tester.view).copyWith( + padding: const EdgeInsets.only(bottom: 34), + viewPadding: const EdgeInsets.only(bottom: 34), + ); + + await _pumpTerminalPage( + tester, + session: _session('session-1', 'codex-main'), + mediaQueryData: mediaQueryData, + ); + + final commandDeckRect = tester.getRect( + find.byKey(const Key('terminal_command_deck')), + ); + + expect(commandDeckRect.bottom, lessThanOrEqualTo(852 - 34)); + }, + ); + + testWidgets('terminal surface keeps terminal content inset from the border', ( + tester, + ) async { + await _pumpTerminalPage( + tester, + session: _session('session-1', 'codex-main'), + ); + + final surfaceRect = tester.getRect( + find.byKey(const Key('terminal_surface_panel')), + ); + final terminalViewRect = tester.getRect(find.byType(TerminalView)); + + expect(terminalViewRect.left, greaterThan(surfaceRect.left)); + expect(terminalViewRect.top, greaterThan(surfaceRect.top)); + expect(terminalViewRect.right, lessThan(surfaceRect.right)); + expect(terminalViewRect.bottom, lessThan(surfaceRect.bottom)); + }); + testWidgets( 'terminal page keeps tools hidden until the user opens the tools sheet', (tester) async { @@ -810,7 +856,16 @@ Future _pumpTerminalPage( required Session session, AgentApiClient? apiClient, TerminalSocketSessionFactory? socketFactory, + MediaQueryData? mediaQueryData, }) async { + Widget page = TerminalPage( + session: session, + agentBaseUri: Uri.parse('http://100.81.30.82:5067'), + ); + if (mediaQueryData != null) { + page = MediaQuery(data: mediaQueryData, child: page); + } + await tester.pumpWidget( ProviderScope( overrides: [ @@ -825,12 +880,7 @@ Future _pumpTerminalPage( ), ), ], - child: MaterialApp( - home: TerminalPage( - session: session, - agentBaseUri: Uri.parse('http://100.81.30.82:5067'), - ), - ), + child: MaterialApp(home: page), ), ); await tester.pumpAndSettle();