feat: quick capture sends to chat instead of quick-capture endpoint

Replace POST /api/quick-capture with: create a conversation, send the
message, and fire-and-forget the SSE generation stream so the assistant
processes the request in the background without blocking the UI.
The new conversation appears immediately in the chat tab.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-06 19:00:33 -04:00
parent 946b70ecc4
commit 4ebc57d2e5
2 changed files with 19 additions and 85 deletions
+1 -48
View File
@@ -5,21 +5,17 @@ import 'package:go_router/go_router.dart';
import 'package:flutter_timezone/flutter_timezone.dart';
import 'core/constants.dart';
import 'core/exceptions.dart';
import 'core/theme.dart';
import 'providers/api_client_provider.dart';
import 'providers/auth_provider.dart';
import 'providers/capture_queue_provider.dart';
import 'providers/capture_work_queue_provider.dart';
import 'providers/briefing_provider.dart';
import 'providers/calendar_provider.dart';
import 'providers/chat_provider.dart';
import 'providers/knowledge_provider.dart';
import 'providers/news_provider.dart';
import 'providers/notes_provider.dart';
import 'providers/settings_provider.dart';
import 'providers/update_provider.dart';
import 'providers/tasks_provider.dart';
import 'screens/auth/login_screen.dart';
import 'screens/briefing/briefing_screen.dart';
import 'screens/knowledge/knowledge_screen.dart';
@@ -516,12 +512,6 @@ class _QuickCaptureBar extends ConsumerStatefulWidget {
class _QuickCaptureBarState extends ConsumerState<_QuickCaptureBar> {
final _controller = TextEditingController();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _drainOfflineQueue());
}
@override
void dispose() {
_controller.dispose();
@@ -537,36 +527,6 @@ class _QuickCaptureBarState extends ConsumerState<_QuickCaptureBar> {
ref.read(captureWorkQueueProvider.notifier).enqueue(text);
}
Future<void> _drainOfflineQueue() async {
if (!mounted) return;
final queue = ref.read(captureQueueProvider);
if (queue.isEmpty) return;
final api = ref.read(quickCaptureApiProvider);
for (final text in List<String>.from(queue)) {
if (!mounted) break;
try {
final result = await api.capture(text);
// Dequeue before the mounted check — SharedPreferences doesn't need
// the widget alive, and skipping this would leave a ghost item.
await ref.read(captureQueueProvider.notifier).dequeue(text);
if (!mounted) break;
switch (result.type) {
case 'note':
ref.invalidate(notesProvider);
case 'task':
case 'todo':
ref.invalidate(tasksProvider);
}
} on NetworkException {
break;
} catch (_) {
// Server error or unexpected failure — drop from queue to prevent
// ghost items that can never be cleared.
await ref.read(captureQueueProvider.notifier).dequeue(text);
}
}
}
Future<void> _toggleCaptureMic() async {
final voice = ref.read(voiceProvider);
if (voice.voiceModeActive) {
@@ -596,10 +556,8 @@ class _QuickCaptureBarState extends ConsumerState<_QuickCaptureBar> {
@override
Widget build(BuildContext context) {
final location = GoRouterState.of(context).matchedLocation;
final offlineQueueCount = ref.watch(captureQueueProvider).length;
final workQueue = ref.watch(captureWorkQueueProvider);
final isWorking = workQueue.isNotEmpty;
final totalPending = workQueue.length + offlineQueueCount;
// Show snackbar when a result is published.
ref.listen(captureResultProvider, (_, result) {
@@ -634,12 +592,7 @@ class _QuickCaptureBarState extends ConsumerState<_QuickCaptureBar> {
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 14, vertical: 10),
prefixIcon: totalPending > 0
? Badge(
label: Text('$totalPending'),
child: const Icon(Icons.cloud_upload_outlined),
)
: isWorking
prefixIcon: isWorking
? const Padding(
padding: EdgeInsets.all(12),
child: SizedBox(