Offline queue (SharedPreferences) persists captures when the device
is offline and replays them as chat conversations on next launch,
preserving the same fire-and-forget guarantee as the online path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Same navigator mismatch as the chat delete bug — outer context inside
showDialog builder resolves to the wrong navigator ancestor.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Navigator.pop(outerContext) inside a showDialog builder resolves to the
ShellRoute's nested navigator instead of the root navigator where the
dialog lives, popping the conversations route and showing a black screen.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds discussArticle() to BriefingApi and wires it through to the
RSS news cards in BriefingScreen so tapping Discuss opens a chat
conversation seeded with the article. Also caps RSS cards per
message at 3 to avoid overly long briefing threads.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The app was creating conversations with title 'New conversation'.
The server only generates a title when conv_title is falsy (empty).
With a non-empty title, should_gen_title is False for the first
message (msg_count % 10 != 0), so auto-naming never fired.
Now creates with empty title (matching web app behaviour). The list
still displays 'New conversation' as a UI placeholder until the
server-generated title arrives.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three bugs causing silent STT failure on Android:
1. AudioEncoder.opus produces an OGG container on Android but the file
was named .webm — faster-whisper rejected it due to format mismatch.
Changed to AudioEncoder.aacLc + .m4a (reliable on all Android versions).
Updated voice_api.dart to send audio/mp4 MIME type accordingly.
2. onError callback was never stored, so errors in _startListening() and
_handleSilence() were silently swallowed. Now stored as _onError and
called before exitVoiceMode() on any failure.
3. Amplitude stream can emit NaN or ±Infinity on some Android devices
during recorder initialisation. NaN < -40.0 is false, so silence was
never detected. Now treated explicitly as silence.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- App resume: invalidates all main providers (conversations, calendar,
news, knowledge, briefing) when the app returns to foreground.
Throttled to once per 5 minutes to avoid hammering the server.
- Tab switch: refreshes the incoming tab's provider when navigating
between Briefing / Knowledge / Conversations shell tabs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tasks are not part of the knowledge API (backend rejects type=task).
When the Tasks tab is selected, fetch from /api/tasks and convert to
KnowledgeItem. Also removes per-type counts from tab labels to prevent
tabs from awkwardly resizing when counts load.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Parse SSE `status` events alongside `chunk` events and display the
status text next to the spinner while the assistant is generating.
Previously the Android app discarded all non-chunk SSE events.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces bare Story-N reaction rows with full NewsCard widgets: source label,
relative timestamp, linked headline, 2-line snippet, and 👍/👎 reactions.
Reads rss_items from message metadata (requires backend ≥ this sprint).
Adds url_launcher ^6.3.1 for opening article links in the browser.
Adds https/http <queries> entries to AndroidManifest for Android 11+.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Timer.periodic every 60s calls silentRefresh() on BriefingNotifier.
silentRefresh() patches AsyncData directly — never triggers AsyncLoading —
so existing content stays on screen while the fetch is in flight.
WidgetsBindingObserver pauses polling when app is backgrounded.
Polling is also skipped while a reply is actively streaming.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On shell mount, reads the IANA timezone from the device via
flutter_timezone and POSTs it to PUT /api/settings as user_timezone.
Mirrors the web app's App.vue behaviour so briefing events and the
chat route use the correct local time on Android.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Android 8+ requires canRequestPackageInstalls() to return true even when
REQUEST_INSTALL_PACKAGES is declared in the manifest. Add permission_handler
and check/request Permission.requestInstallPackages before downloading;
redirects user to Settings if not granted and shows a clear error message.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The global receiveTimeout is 30s but quick capture runs LLM inference
that can take much longer. receiveTimeout fired, fell through to the
NetworkException fallback in dioToApp, and the work queue queued it as
an offline item.
- quick_capture_api.dart: override receiveTimeout to 120s for the
/api/quick-capture request
- api_client.dart: handle receiveTimeout/sendTimeout explicitly in the
error interceptor, converting them to AppException (not NetworkException)
so slow responses are never mistaken for being offline
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>