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
| .. | .. |
|---|
| 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 |
|---|
| .. | .. |
|---|
| 364 | 364 | } |
|---|
| 365 | 365 | |
|---|
| 366 | 366 | 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 |
|---|
| 368 | 368 | await AudioService.stopPlayback(); |
|---|
| 369 | 369 | setState(() => _playingMessageId = null); |
|---|
| 370 | + if (mounted) FocusScope.of(context).unfocus(); |
|---|
| 370 | 371 | |
|---|
| 371 | 372 | ref.read(activeSessionIdProvider.notifier).state = sessionId; |
|---|
| 372 | 373 | await ref.read(messagesProvider.notifier).switchSession(sessionId); |
|---|
| .. | .. |
|---|
| 391 | 392 | |
|---|
| 392 | 393 | ref.read(messagesProvider.notifier).addMessage(message); |
|---|
| 393 | 394 | _textController.clear(); |
|---|
| 395 | + FocusScope.of(context).unfocus(); // dismiss keyboard |
|---|
| 394 | 396 | |
|---|
| 395 | 397 | // Send as plain text (not command) — gateway handles plain messages |
|---|
| 396 | 398 | _ws?.send({ |
|---|
| .. | .. |
|---|
| 822 | 824 | final unreadCounts = ref.watch(unreadCountsProvider); |
|---|
| 823 | 825 | final inputMode = ref.watch(inputModeProvider); |
|---|
| 824 | 826 | |
|---|
| 825 | | - return Scaffold( |
|---|
| 827 | + return GestureDetector( |
|---|
| 828 | + onTap: () => FocusScope.of(context).unfocus(), |
|---|
| 829 | + child: Scaffold( |
|---|
| 826 | 830 | key: _scaffoldKey, |
|---|
| 827 | 831 | appBar: AppBar( |
|---|
| 828 | 832 | leading: IconButton( |
|---|
| .. | .. |
|---|
| 927 | 931 | ), |
|---|
| 928 | 932 | ], |
|---|
| 929 | 933 | ), |
|---|
| 934 | + ), |
|---|
| 930 | 935 | ); |
|---|
| 931 | 936 | } |
|---|
| 932 | 937 | } |
|---|
| .. | .. |
|---|
| 111 | 111 | const EdgeInsets.symmetric(horizontal: 16, vertical: 10), |
|---|
| 112 | 112 | isDense: true, |
|---|
| 113 | 113 | ), |
|---|
| 114 | | - textInputAction: TextInputAction.send, |
|---|
| 115 | | - onSubmitted: (_) => onSendText(), |
|---|
| 114 | + textInputAction: TextInputAction.newline, |
|---|
| 116 | 115 | maxLines: 4, |
|---|
| 117 | 116 | minLines: 1, |
|---|
| 118 | 117 | ), |
|---|