Matthias Nott
2026-03-22 ef77858d82f6ae6fc397d56546105b014eab2aea
fix: keyboard dismiss on tap outside, line breaks, session switch

- GestureDetector wraps Scaffold to dismiss keyboard on tap (keeps text)
- Return key inserts line breaks (TextInputAction.newline)
- Keyboard dismissed after sending and on session switch
- Audio chain playback rewritten with proper queue
- Play button state resets via onPlaybackStateChanged callback
- Empty text bubbles filtered on load
1 files added
2 files modified
changed files
TODO.md patch | view | blame | history
lib/screens/chat_screen.dart patch | view | blame | history
lib/widgets/input_bar.dart patch | view | blame | history
TODO.md
....@@ -0,0 +1,37 @@
1
+# PAILot Flutter - TODO
2
+
3
+## Pending Features
4
+
5
+### File Transfer (send/receive arbitrary files)
6
+- File picker in app (PDFs, Word docs, attachments, etc.)
7
+- New `file` message type in WebSocket protocol
8
+- Gateway handler to save received files and route to session
9
+- Session can send files back via `pailot_send_file`
10
+- Display file attachments in chat bubbles (icon + filename + tap to open)
11
+
12
+### Voice+Image Combined Message
13
+- When voice caption is recorded with images, hold images on server until voice transcript arrives
14
+- Deliver transcript + images together as one message to Claude
15
+- Ensures voice prefix sets reply channel correctly
16
+
17
+### Push Notifications (iOS APNs)
18
+- Notify when messages arrive while app is backgrounded/closed
19
+- Requires Apple Developer Portal APNs key setup
20
+- Server-side message queue for offline delivery
21
+
22
+### App Name Renaming (Runner → PAILot)
23
+- Rename Xcode target from Runner to PAILot (like Glidr did)
24
+- Update scheme names, bundle paths
25
+
26
+## Known Issues
27
+
28
+### Audio
29
+- Background audio may not survive full app termination (only screen lock)
30
+- Audio session category may conflict with phone calls
31
+
32
+### UI
33
+- Launch image still uses default Flutter placeholder
34
+- No app splash screen with PAILot branding
35
+
36
+### Navigation
37
+- vi keys (0, G, dd) are sent as literal text paste — works for Claude Code but may not for other terminals
lib/screens/chat_screen.dart
....@@ -364,9 +364,10 @@
364364 }
365365
366366 Future<void> _switchSession(String sessionId) async {
367
- // Stop any playing audio when switching sessions
367
+ // Stop any playing audio and dismiss keyboard when switching sessions
368368 await AudioService.stopPlayback();
369369 setState(() => _playingMessageId = null);
370
+ if (mounted) FocusScope.of(context).unfocus();
370371
371372 ref.read(activeSessionIdProvider.notifier).state = sessionId;
372373 await ref.read(messagesProvider.notifier).switchSession(sessionId);
....@@ -391,6 +392,7 @@
391392
392393 ref.read(messagesProvider.notifier).addMessage(message);
393394 _textController.clear();
395
+ FocusScope.of(context).unfocus(); // dismiss keyboard
394396
395397 // Send as plain text (not command) — gateway handles plain messages
396398 _ws?.send({
....@@ -822,7 +824,9 @@
822824 final unreadCounts = ref.watch(unreadCountsProvider);
823825 final inputMode = ref.watch(inputModeProvider);
824826
825
- return Scaffold(
827
+ return GestureDetector(
828
+ onTap: () => FocusScope.of(context).unfocus(),
829
+ child: Scaffold(
826830 key: _scaffoldKey,
827831 appBar: AppBar(
828832 leading: IconButton(
....@@ -927,6 +931,7 @@
927931 ),
928932 ],
929933 ),
934
+ ),
930935 );
931936 }
932937 }
lib/widgets/input_bar.dart
....@@ -111,8 +111,7 @@
111111 const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
112112 isDense: true,
113113 ),
114
- textInputAction: TextInputAction.send,
115
- onSubmitted: (_) => onSendText(),
114
+ textInputAction: TextInputAction.newline,
116115 maxLines: 4,
117116 minLines: 1,
118117 ),