Previously, if the backend was unreachable at launch, the splash screen
routed to the login screen. The server URL remained persisted in
SharedPreferences but visually appeared lost, frustrating any user who
isn't the service operator.
- New AuthStatus.offline distinguishes network failures (NetworkException)
from HTTP 401 in AuthNotifier.verify().
- Persist has_ever_logged_in flag on first successful verify/login.
- Offline + ever-logged-in lands on the briefing with a sticky offline
banner (retry button) instead of being punted to login.
- OfflineBanner widget is Tier-2-ready so we can surface "last sync X min
ago" once real caching lands (Fable task #147).
Fetch server-side settings on app launch via new serverSettingsProvider.
Hide News from bottom sheet and NavigationRail when RSS is disabled.
Skip news card rendering in briefing messages when disabled.
Refactor tab navigation to use dynamic tabs list and route-based refresh.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update provider: auto-downloads APK in background after finding a newer
version, prompts via snackbar only when ready, cleans up old APKs on
startup. Replaces modal dialog with dismissible snackbar.
Chat bubble: resolve relative image URLs (/api/images/{id}) against
server base URL with auth cookies so search_images results render on
the phone app.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
VadHandler is now the sole mic owner — removed separate AudioRecorder that
caused audio focus contention on Android. Audio from onSpeechEnd is encoded
as WAV for Whisper. Provider switched to autoDispose to match widget lifecycle,
preventing defunct element assertions. Recording UI deferred until mic is open.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use VadHandler from the vad package (Silero VAD v5 ONNX) for speech
detection instead of amplitude thresholds. VAD runs its own PCM
stream while the file recorder captures AAC-LC for Whisper. Grace
period starts on speech-start, auto-stop on speech-end after grace.
No-speech guard shows error on manual stop without detected speech.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Shell: move Projects/News/Calendar into ShellRoute so the NavigationRail
persists across all screens. Show all 6 nav destinations on tablet
instead of 3+More overflow. Phone bottom nav unchanged.
Chat: master-detail layout on tablet — conversations list (320px) with
inline chat panel. Tapping a conversation updates state instead of
pushing a new route.
Knowledge: responsive grid (2 cols portrait, 3 cols landscape) with
card layout showing icon, title, body preview, and tags. Fix snippet
data — read json['snippet'] which the API actually sends. Bump snippet
length from 120 to 200 chars. Tasks now show description as snippet.
News: responsive grid with the same breakpoints. Larger snippet
(5 lines) in grid mode via snippetMaxLines parameter.
Briefing: centered reading column (maxWidth 700) on wide screens,
matching the web UI layout.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous -40 dBFS static threshold sat right on top of typical
phone mic ambient, so silence detection rarely fired and the user
always had to tap stop manually.
Silence threshold is now dynamic: track the session peak dBFS and
treat "silent" as 15 dB below peak. Auto-calibrates per mic and
environment rather than assuming a fixed ambient level.
- Grace period (1500 ms) at start so the user has time to begin
speaking before checks arm.
- Static -35 dB fallback until the peak clears -20 dB so a dead-
silent session doesn't spin forever.
- Silence duration bumped 1500 → 2000 ms for breathing room.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
VoiceState now carries a normalized mic amplitude (0..1) updated
from the existing onAmplitudeChanged subscription, with a 0.02
change threshold so we don't spam rebuilds.
VoiceMicButton swaps the constant-rate AnimationController for an
AnimatedScale + animated glow driven by the live amplitude. 0.1
floor keeps the button breathing on silence; 120ms ease-out smooths
between 200ms samples.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Purple-on-black was eye-catching but hard to read. Switch title text
to onSurface for contrast; keep the underline in primary at 70% alpha
as the "this is a link" signal.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Mirrors the same fix applied to the fabledassistant repo: merges to
main only happen after dev already passed CI and tagged releases are
the sole trigger for a signed APK build, so re-running analyze+test on
the merge commit just burns runner time.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Briefing chat now uses the same StreamIterator + 45s per-event timeout
as the main chat provider, so a dropped SSE socket no longer leaves
isBriefingStreaming stuck true. Adds refreshMessages() that unfreezes
state when the server-side message is complete, wired to a new
pull-to-refresh gesture and the app lifecycle-resume hook.
Chat provider gains attachToGeneration() — safe to call unconditionally
on screen init, so landing on a conversation that's already mid-stream
(e.g. the /news discuss button, which now creates a conv and auto-kicks
generation server-side) picks up live tokens instead of freezing on the
placeholder.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Mobile chat occasionally froze with the input disabled and nothing
progressing — the underlying cause is SSE sockets dropping silently on
mobile network handoffs / proxy idle timeouts. Dio never observes the
close, so the send loop's `await for` hangs indefinitely with
isStreaming=true.
Three changes:
- Wrap the SSE stream in a StreamIterator with a 45s per-event timeout
as a stall watchdog. On timeout, break out and let the polling pass
reconcile state from the server.
- Add pull-to-refresh on the chat message list and an AppBar refresh
button with inline spinner feedback. Works on both empty and
populated states.
- MessagesNotifier.refresh() now also clears isStreaming /
streamingStatus when the server reports the latest assistant message
as complete — so a stuck UI unfreezes the moment the user pulls down
or taps refresh, even if the original SSE loop is still hanging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Chips now show the entity title ("Created note: Grocery List") and
deep-link to the matching screen when tapped — notes to the detail
screen, tasks to the edit screen, projects to the project tasks view,
events to the calendar. Read-only tools and errors stay non-tappable.
Closes the main goal behind the mobile chip work: seeing that a note
was created and jumping straight to it without backing out to the
library.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The mobile app was receiving SSE status events but showing the peek text
only while the assistant bubble was empty — as soon as the first token
arrived, the status line was replaced by streaming content and any tool
calls fired mid-turn left no trace. Tool call SSE events were also being
dropped by the parser, and Message.fromJson never read the persisted
tool_calls array, so chips never rendered after reload either.
Parse tool_call SSE frames into a new ChatToolCall event, carry tool
calls on Message, and update the chat and briefing streaming loops to
append chips to the in-flight assistant message as they arrive. Rework
ChatMessageBubble to show a chip row + rolling peek status line above
any streamed text, matching the web ToolCallCard/status indicator
behaviour across chat, briefing, and briefing history surfaces.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Voice: recreate AudioRecorder each session to avoid stale native
state, improve permission handling (re-check after settings), show
feedback after 3 consecutive empty transcripts, allow STT-only mode
when TTS is unavailable.
Knowledge: hydrate IDs immediately after loading more pages so
scroll-based pagination doesn't stall at the bottom.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- trigger: add branches: [dev, main] so analyze+test run on every
push, not just release tags. Build job stays tag-gated via
if: startsWith(github.ref, 'refs/tags/')
- pin Flutter to 3.41.6 instead of floating :stable for reproducible
release builds
- concurrency: cancel in-progress non-tag runs
- permissions: contents: read default, contents: write scoped to build
- extract release publish logic to scripts/publish_apk_release.sh so
it's testable locally (bash -x with env vars) and the YAML stays
readable. Adds set -euo pipefail + curl -f so failures surface
instead of getting swallowed by || echo "skipped"
- drop the broken artifact upload step (silently swallowed errors)
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>
Replaces outdated Library/stub descriptions with accurate coverage of
Knowledge, Calendar, News, Voice I/O, chat tool-status notifications,
auto-refresh, and the current file tree.
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>