Skip to content

fix: share chat exports instead of app-private path#3

Merged
involvex merged 3 commits into
mainfrom
fix/chat-export-share
Jul 18, 2026
Merged

fix: share chat exports instead of app-private path#3
involvex merged 3 commits into
mainfrom
fix/chat-export-share

Conversation

@involvex

@involvex involvex commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Summary

  • Chat export no longer writes to /data/user/0/.../app_flutter
  • Opens the system share sheet via ExportService (same path as tasks/notes)

Test plan

  • Chat History → Export opens share sheet
  • Assistant toolbar → Export as Text / JSON opens share sheet
  • flutter test test/services/chat_history_export_test.dart

Summary by Sourcery

Export chat history as in-memory text/JSON for sharing instead of writing app-private files, improve voice input behavior, and enhance natural-language handling for alarms and app-opening tools.

New Features:

  • Support live partial transcripts and safer final transcript handling in the voice input button.
  • Introduce deterministic open-app parsing from natural-language queries to Android package names, including German variants.

Bug Fixes:

  • Prevent chat exports from writing to app-private storage paths and instead return content for use with the existing export/share flow.
  • Avoid prematurely sending partial speech recognition results that left remaining text stuck in the input when a reply was already generating.
  • Extend alarm time parsing to correctly interpret German alarm phrases and "Uhr"-style times.
  • Ensure the model prompt consistently advertises device tool capabilities so it does not incorrectly claim lack of device access.

Enhancements:

  • Reuse shared message formatting in chat export helpers and clarify their responsibilities via documentation comments.
  • Improve the open_app tool description with clearer guidance and common Android package examples.
  • Wire chat exports and conversation exports to the shared ExportService and update UI feedback to reflect share sheet usage instead of file paths.

Tests:

  • Add unit tests for chat history text export behavior (content-only, no file path assumptions).
  • Add unit tests for German alarm parsing cases.
  • Add unit tests for the new open-app intent parser in English and German.

involvex and others added 2 commits July 18, 2026 03:51
Accumulate STT until listening ends so early finalResult no longer sends a partial message. Add open-app/alarm intent shortcuts and restore tool capability text when native FC is disabled on Android Gemma 4.

Co-authored-by: Cursor <cursoragent@cursor.com>
Normalize umlauts so geöffnet is not treated as an open intent, gate device-tool capability text to Android, and align beginner voice busy checks with the main chat screen.

Co-authored-by: Cursor <cursoragent@cursor.com>
@changeset-bot

changeset-bot Bot commented Jul 18, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: e679397

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@sourcery-ai

sourcery-ai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Reviewer's Guide

Refactors chat export to return in-memory text/JSON content for sharing instead of writing app-private files, improves voice input behavior and partial transcription handling, and enhances on-device intent parsing for alarms and app-opening (including German language support) with accompanying tests and prompt wiring.

Sequence diagram for chat export sharing flow

sequenceDiagram
  actor User
  participant AssistantScreen
  participant ChatHistoryService
  participant ExportService
  participant SystemShareSheet

  User->>AssistantScreen: tap Export conversation
  AssistantScreen->>ChatHistoryService: exportAsText()/exportAsJson()
  ChatHistoryService-->>AssistantScreen: String content
  AssistantScreen->>ExportService: shareText(content, fileName)
  ExportService-->>SystemShareSheet: invoke share sheet
  SystemShareSheet-->>User: choose target app
Loading

Sequence diagram for updated voice input and partial transcription

sequenceDiagram
  actor User
  participant VoiceInputButton
  participant SpeechToText
  participant AssistantScreen

  User->>VoiceInputButton: press and hold
  VoiceInputButton->>SpeechToText: listen(SpeechListenOptions)
  SpeechToText-->>VoiceInputButton: onResult(partial/final)
  VoiceInputButton->>AssistantScreen: onPartial(lastWords)
  AssistantScreen->>AssistantScreen: update _inputController

  User-->>VoiceInputButton: release or pause
  SpeechToText-->>VoiceInputButton: onStatus(done / notListening)
  VoiceInputButton->>VoiceInputButton: _finishListening()
  VoiceInputButton->>SpeechToText: stop()
  VoiceInputButton->>AssistantScreen: onTranscription(transcript)
  AssistantScreen->>AssistantScreen: set _inputController.text
  AssistantScreen->>AssistantScreen: _sendMessage() [if not generating]
Loading

Sequence diagram for open app intent handling and tool execution

sequenceDiagram
  actor User
  participant ModelOrchestrator
  participant OpenAppIntentParser
  participant ToolExecutorService

  User->>ModelOrchestrator: send query
  ModelOrchestrator->>OpenAppIntentParser: tryParsePackage(query)
  OpenAppIntentParser-->>ModelOrchestrator: packageName/null
  alt packageName found
    ModelOrchestrator->>ToolExecutorService: openApp(packageName)
    ToolExecutorService-->>ModelOrchestrator: result{success,error}
    ModelOrchestrator-->>User: InferenceResult(text, toolCalls)
  else no package
    ModelOrchestrator->>ModelOrchestrator: buildSystemInstruction(..., _deviceToolsCapabilitySuffix)
    ModelOrchestrator-->>User: normal LLM handling
  end
Loading

File-Level Changes

Change Details Files
Refactor chat history export to produce in-memory text/JSON and wire it to ExportService-based sharing in history and assistant screens.
  • Change chat export methods to build and return plain-text or JSON content without touching the filesystem.
  • Extract common message-to-text formatting into a private helper for reuse across export paths.
  • Update assistant and chat history screens to use ExportService.shareText with appropriate filenames instead of showing raw file paths, and improve user-facing SnackBars for empty/failed exports.
lib/services/chat_history_service.dart
lib/screens/assistant_screen.dart
lib/screens/chat_history_screen.dart
test/services/chat_history_export_test.dart
Improve voice input UX to support live partial transcription, robust flush-on-end, and avoid prematurely sending partial speech while a reply is generating.
  • Extend VoiceInputButton to accept an optional onPartial callback and track a flushing state to avoid double-finishing.
  • Route speech status callbacks to a dedicated finish method that trims and sends the final transcript once when listening ends.
  • Accumulate recognizedWords during listening, invoke onPartial for live previews, and lengthen listen/pause durations.
  • In assistant screens, update the text field with partial/final transcripts and only auto-send when not already generating or busy.
lib/widgets/voice_input.dart
lib/screens/assistant_screen.dart
lib/screens/assistant_screen_beginner.dart
Add deterministic on-device open-app handling and improve device-tool capability prompting, including better alarm and app intent parsing (with German support).
  • Introduce OpenAppIntentParser to map natural-language open requests (English/German) to known Android package names with umlaut normalization and phrase handling.
  • Invoke open-app parsing early in ModelOrchestrator.infer and call ToolExecutorService.openApp directly when a supported app is detected, yielding a non-streaming tool result.
  • Pass text-only tools into the system prompt builder when function-calling is unavailable and append a capability suffix describing available device tools and JSON-only fallback format.
  • Expand AlarmTimeParser to recognize German alarm phrases and time formats (e.g., “Wecker um 19:00 / auf 7 Uhr”).
  • Clarify the open_app tool definition with English/German trigger verbs and common package examples, and add unit tests for the new parsers and alarm cases.
lib/services/model_orchestrator.dart
lib/utils/open_app_intent_parser.dart
lib/utils/alarm_time_parser.dart
lib/tools/tool_definitions.dart
test/utils/open_app_intent_parser_test.dart
test/utils/alarm_time_parser_test.dart

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Stop writing nova_export files into the app-private documents path. Build export content in memory and open the existing Android share chooser so users can save to Files, Drive, or other apps.

Co-authored-by: Cursor <cursoragent@cursor.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • In OpenAppIntentParser.tryParsePackage, using lower.contains(alias) without word boundaries can match substrings inside unrelated words (e.g. chrome in a longer token); consider tightening this with tokenization or regex word boundaries to reduce false positives.
  • The export flows now always show a "Share sheet opened" SnackBar immediately after calling ExportService.instance.shareText(...) without checking the outcome; it may be more accurate to react to a success/failure signal from ExportService (or catch exceptions) so the UI reflects actual share status.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `OpenAppIntentParser.tryParsePackage`, using `lower.contains(alias)` without word boundaries can match substrings inside unrelated words (e.g. `chrome` in a longer token); consider tightening this with tokenization or regex word boundaries to reduce false positives.
- The export flows now always show a "Share sheet opened" SnackBar immediately after calling `ExportService.instance.shareText(...)` without checking the outcome; it may be more accurate to react to a success/failure signal from `ExportService` (or catch exceptions) so the UI reflects actual share status.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the conversation export feature to return content directly instead of writing to a file, utilizing ExportService to trigger a share sheet. It also introduces a deterministic 'open app' shortcut parser supporting English and German, adds German support to the alarm parser, and improves the voice input button with partial transcription support. The review feedback suggests enhancing the German open-app regex to support multiple words and using word boundaries for app alias matching to prevent false positives.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +11 to +14
static final RegExp _machAufIntent = RegExp(
r'\bmach(?:en)?\s+\S+\s+auf\b',
caseSensitive: false,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current regular expression \bmach(?:en)?\s+\S+\s+auf\b only matches exactly one non-whitespace word between 'mach' and 'auf'. In natural German speech, users often insert filler words (e.g., 'mach bitte youtube auf' or 'mach mal die kamera auf').

Using [^.!?]+? instead of \S+ allows matching multiple words within the same sentence, making the intent parser much more robust.

Suggested change
static final RegExp _machAufIntent = RegExp(
r'\bmach(?:en)?\s+\S+\s+auf\b',
caseSensitive: false,
);
static final RegExp _machAufIntent = RegExp(
r'\bmach(?:en)?\s+[^.!?]+?\s+auf\b',
caseSensitive: false,
);

Comment on lines +57 to +71
static String? tryParsePackage(String query) {
if (!_hasOpenIntent(query)) return null;

final lower = query.toLowerCase();
// Prefer longer aliases first (e.g. "google maps" before "maps").
final aliases = knownApps.keys.toList()
..sort((a, b) => b.length.compareTo(a.length));
for (final alias in aliases) {
if (lower.contains(alias)) {
return knownApps[alias];
}
}

return null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using lower.contains(alias) for matching app aliases can lead to false positives when an alias is a substring of another word (for example, 'monochrome' matching 'chrome', or 'bitmaps' matching 'maps').

Using a regular expression with word boundaries (\b) ensures that aliases are only matched as complete words, preventing accidental false positive matches.

  static String? tryParsePackage(String query) {
    if (!_hasOpenIntent(query)) return null;

    final lower = query.toLowerCase();
    // Prefer longer aliases first (e.g. "google maps" before "maps").
    final aliases = knownApps.keys.toList()
      ..sort((a, b) => b.length.compareTo(a.length));
    for (final alias in aliases) {
      final regex = RegExp(r'\b' + RegExp.escape(alias) + r'\b', caseSensitive: false);
      if (regex.hasMatch(lower)) {
        return knownApps[alias];
      }
    }

    return null;
  }

@involvex
involvex merged commit e679397 into main Jul 18, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant