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>
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>
Without a consistent signing key, each CI build was signed with the
ephemeral debug keystore from the Flutter container — causing Android
to reject updates with INSTALL_FAILED_UPDATE_INCOMPATIBLE.
Secrets stored in repo: RELEASE_KEYSTORE_BASE64, RELEASE_KEYSTORE_PASSWORD,
RELEASE_KEY_ALIAS. CI decodes the keystore and writes key.properties
before building so every release APK is signed identically.
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>
- ProjectLibraryCard.onTap navigates to /projects/:id/tasks
- New ProjectTasksScreen: milestone sections with coloured dot,
done/total counter and thin progress bar; unassigned tasks at bottom
- Status cycle icon updates optimistically (pendingStatus map) so
the list doesn't flash on every tap; syncs global tasksProvider
- New route /projects/:id/tasks registered in app.dart
- TasksApi.getByProject + TasksRepository + projectTasksProvider (family)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Check OpenResult.type before resetting state — previously a failed
open_file call (e.g. installer blocked on emulator) silently called
state = const UpdateState(), nulling latestVersion and downloadUrl,
causing "Version null" and a non-functional Download button
- Show errorMessage in dialog with error colour so failures are visible
- Guard Download button on downloadUrl != null (no button if URL lost)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The summary card duplicated the first assistant message and consumed
screen real estate without benefit on small screens. Replaced with:
- Full message list via ChatMessageBubble from the top
- SliverFillRemaining empty state ("No briefing yet today" + generate button)
when no messages exist
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Update dialog no longer re-appears on every shell re-mount; check()
is skipped if status is already available/upToDate/downloading
- Offline queue dequeue now happens before the mounted check, preventing
ghost items when the widget disposes mid-drain
- Briefing digest card and messages now share a CustomScrollView so
expanding the card doesn't trap the user without scroll access
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>