Skip to content

fix: voice STT full transcript and German device tools#2

Merged
involvex merged 2 commits into
mainfrom
fix/voice-stt-and-tools-a31e
Jul 18, 2026
Merged

fix: voice STT full transcript and German device tools#2
involvex merged 2 commits into
mainfrom
fix/voice-stt-and-tools-a31e

Conversation

@involvex

@involvex involvex commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Summary

  • Fix voice STT sending only an early partial transcript; accumulate until listening ends
  • Add German open-app / alarm intent shortcuts and restore tool capability text when Android Gemma 4 disables native FC
  • Harden open-intent matching (no false positive on geöffnet); Android-only capability suffix; beginner isBusy guard

Test plan

  • Speak a long German sentence via mic — full text should send once
  • öffne YouTube / stell einen Wecker um 19:00 should act without model refusal
  • Ist YouTube schon geöffnet? should NOT open YouTube
  • flutter test test/utils/open_app_intent_parser_test.dart test/utils/alarm_time_parser_test.dart

Summary by Sourcery

Improve voice input transcription reliability and add deterministic Android device tool handling for German and English open-app and alarm requests.

New Features:

  • Provide optional live partial speech transcription updates while listening and send a single full transcript when recording ends.
  • Add deterministic parsing of English and German open-app intents into known Android package names and invoke the open_app tool directly when matched.
  • Extend alarm time parsing to handle German alarm phrases and times such as "stell einen Wecker um 19:00" and "Wecker auf 7 Uhr".
  • Restore and augment Android device tool capability text in compact/system prompts when native function calling is unavailable.

Bug Fixes:

  • Ensure long speech-to-text input sends the complete transcript instead of an early partial result and avoid duplicate flushes when the engine auto-stops.
  • Prevent false-positive open-app triggers on German phrases like "geöffnet" by tightening open-intent matching.
  • Avoid auto-sending voice transcripts while a response is already being generated, leaving text in the input field instead.

Enhancements:

  • Clarify open_app tool descriptions with common Android package examples and German verbs to guide models toward correct usage.
  • Adjust speech listening timeouts and pause durations to better accommodate longer utterances.
  • Add beginner-mode guarding against concurrent model activity when triggering voice-sent messages.

Tests:

  • Add unit tests for German alarm phrases and "Uhr" time formats in the alarm time parser.
  • Add unit tests for the open-app intent parser covering English and German verbs, aliases, and negative cases like "geöffnet".

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>
@sourcery-ai

sourcery-ai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements more robust voice input handling (accumulated STT transcript + live preview), deterministic Android open-app and alarm shortcuts with German support, and restores on-device tool capabilities in compact prompts, plus corresponding tests and beginner-mode guards.

Sequence diagram for updated voice STT accumulation and send behavior

sequenceDiagram
  actor User
  participant VoiceInputButton
  participant SpeechToText as SpeechToText
  participant AssistantScreen
  participant ModelOrchestrator

  User->>VoiceInputButton: press and hold
  VoiceInputButton->>SpeechToText: listen(SpeechListenOptions)
  SpeechToText-->>VoiceInputButton: onResult(result)
  VoiceInputButton->>VoiceInputButton: _lastWords = result.recognizedWords
  VoiceInputButton-->>AssistantScreen: onPartial(_lastWords)
  AssistantScreen->>AssistantScreen: _inputController.text = partial

  User->>VoiceInputButton: release / engine pauseFor
  SpeechToText-->>VoiceInputButton: onStatus(done | notListening)
  VoiceInputButton->>VoiceInputButton: _finishListening()
  VoiceInputButton->>SpeechToText: stop()
  VoiceInputButton->>VoiceInputButton: transcript = _lastWords.trim()
  VoiceInputButton-->>AssistantScreen: onTranscription(transcript)
  AssistantScreen->>AssistantScreen: _inputController.text = transcript
  AssistantScreen->>AssistantScreen: [if _isGenerating or ModelOrchestrator.isBusy] keep text only
  AssistantScreen->>ModelOrchestrator: _sendMessage() [else]
  ModelOrchestrator-->>AssistantScreen: response
Loading

File-Level Changes

Change Details Files
Accumulate full STT transcript and separate live preview from final send in voice input.
  • Add optional onPartial callback to VoiceInputButton and wire it to update text fields in assistant screens.
  • Accumulate recognition results in _lastWords during listening and only send the final transcript when listening ends, guarding against engine auto-stop via status callbacks.
  • Introduce _finishListening/_flushing to ensure the transcript is flushed exactly once and extend listenFor/pauseFor durations for better dictation UX.
  • Prevent auto-sending voice messages while a response is generating or the orchestrator is busy; leave text in the input instead.
lib/widgets/voice_input.dart
lib/screens/assistant_screen.dart
lib/screens/assistant_screen_beginner.dart
Add deterministic open-app parsing (incl. German) and route matching queries directly to the open_app tool.
  • Introduce OpenAppIntentParser to detect explicit open-intent phrases (English and German) and map known app aliases to Android package names while avoiding false positives like "geöffnet".
  • Add tests to validate English/German open-intent parsing, alias mapping, and non-open queries returning null.
  • In ModelOrchestrator, short-circuit user queries matching an open-app intent to call ToolExecutorService.openApp and yield a non-streaming InferenceResult with a synthetic tool call record.
lib/utils/open_app_intent_parser.dart
test/utils/open_app_intent_parser_test.dart
lib/services/model_orchestrator.dart
Extend alarm time parsing with German phrases and add tests.
  • Broaden alarm intent regex to recognize common German alarm phrases (Wecker/Alarm stellen, weck mich, etc.).
  • Add a German-specific time pattern to parse expressions like "Wecker auf 7 Uhr" or "stell einen Wecker um 19:00" and integrate it into tryParse.
  • Add unit tests for the new German alarm formats.
lib/utils/alarm_time_parser.dart
test/utils/alarm_time_parser_test.dart
Restore explicit Android device tool capability text in compact prompts and when only text tools are available.
  • Thread an optional textTools list into _buildSystemPrompt and gate a new device tools capability suffix on Android-only compact prompts or non-empty text tools.
  • Emit a short capability block describing available device tools and example packages, including a JSON-only format when native function calling is unavailable.
  • Use chatTools emptiness and tools presence to decide when to pass text tools for inclusion in the system prompt.
lib/services/model_orchestrator.dart
Clarify open_app tool description and parameters for Android and German usage.
  • Update open_app description to mention German trigger verbs and common Android package examples.
  • Tighten parameter description to emphasize Android package names (e.g., com.google.android.youtube).
lib/tools/tool_definitions.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

@changeset-bot

changeset-bot Bot commented Jul 18, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: fbde0cf

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

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>

@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 found 3 issues, and left some high level feedback:

  • In OpenAppIntentParser.tryParsePackage, matching aliases via lower.contains(alias) can produce false positives for substrings inside longer words (e.g. chrome in chromebook); consider enforcing word boundaries or token-based matching for aliases to reduce accidental app resolutions.
  • In _startListening you await _speech.listen but ignore its boolean result; consider handling the failure case (e.g. returning early and resetting _isListening) so the UI state doesn’t get stuck if listening cannot actually start.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `OpenAppIntentParser.tryParsePackage`, matching aliases via `lower.contains(alias)` can produce false positives for substrings inside longer words (e.g. `chrome` in `chromebook`); consider enforcing word boundaries or token-based matching for aliases to reduce accidental app resolutions.
- In `_startListening` you `await _speech.listen` but ignore its boolean result; consider handling the failure case (e.g. returning early and resetting `_isListening`) so the UI state doesn’t get stuck if listening cannot actually start.

## Individual Comments

### Comment 1
<location path="lib/services/model_orchestrator.dart" line_range="1583-1579" />
<code_context>
       }

+      // Deterministic open-app shortcut (YouTube, Settings, etc.).
+      final openPackage = OpenAppIntentParser.tryParsePackage(query);
+      if (openPackage != null) {
+        _statusController.add('Opening app...');
+        final result = await ToolExecutorService.instance.openApp(openPackage);
+        final ok = result['success'] == true;
+        yield InferenceResult(
+          text: ok
+              ? 'Opened $openPackage.'
+              : 'Could not open the app: ${result['error'] ?? 'unknown error'}',
+          model: selector.primaryHeavy,
+          isStreaming: false,
+          toolCalls: [
+            {
+              'name': 'open_app',
+              'args': {'package': openPackage},
+              'result': result,
+            },
+          ],
+        );
+        return;
+      }
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Open-app shortcut currently bypasses any confirmation or disambiguation, which may be surprising on ambiguous queries.

Because this shortcut fires on any query that parses as both an open intent and a known alias, it will also trigger on informational prompts like “Explain how to open YouTube settings”, issuing an `open_app` call instead of letting the model respond. Consider tightening the heuristic (e.g., require the intent and alias to be close/within the same clause, or skip obvious “how to”/explanatory phrasing) so purely informational queries don’t invoke the tool.
</issue_to_address>

### Comment 2
<location path="lib/utils/open_app_intent_parser.dart" line_range="17-26" />
<code_context>
+  );
+
+  /// Common app aliases → package names.
+  static const Map<String, String> knownApps = {
+    'youtube': 'com.google.android.youtube',
+    'yt': 'com.google.android.youtube',
+    'settings': 'com.android.settings',
+    'einstellungen': 'com.android.settings',
+    'chrome': 'com.android.chrome',
+    'maps': 'com.google.android.apps.maps',
+    'google maps': 'com.google.android.apps.maps',
+    'kamera': 'com.android.camera',
+    'camera': 'com.android.camera',
+    'photos': 'com.google.android.apps.photos',
+    'galeria': 'com.google.android.apps.photos',
+    'gallery': 'com.google.android.apps.photos',
+    'whatsapp': 'com.whatsapp',
+    'spotify': 'com.spotify.music',
+    'clock': 'com.android.deskclock',
+    'uhr': 'com.android.deskclock',
+    'wecker': 'com.android.deskclock',
+  };
+
</code_context>
<issue_to_address>
**suggestion:** Review alias list and matching strategy for coverage and accidental matches.

Two small follow‑ups:
- Confirm whether `'galeria'` is intentional; if so, consider also adding `'galerie'`.
- `lower.contains(alias)` may match aliases as substrings inside longer words. For some entries (e.g. WhatsApp), consider using word‑boundary‑style matching (like `\bwhatsapp\b`) to reduce accidental matches without much added complexity.

Suggested implementation:

```
  /// Common app aliases → package names.
  ///
  /// Note:
  /// - `"galeria"` is intentionally kept as a Spanish/Portuguese variant.
  /// - `"galerie"` is added as the German variant.
  static const Map<String, String> knownApps = {
    'youtube': 'com.google.android.youtube',
    'yt': 'com.google.android.youtube',
    'settings': 'com.android.settings',
    'einstellungen': 'com.android.settings',
    'chrome': 'com.android.chrome',
    'maps': 'com.google.android.apps.maps',
    'google maps': 'com.google.android.apps.maps',
    'kamera': 'com.android.camera',
    'camera': 'com.android.camera',
    'photos': 'com.google.android.apps.photos',
    'galeria': 'com.google.android.apps.photos',
    'galerie': 'com.google.android.apps.photos',
    'gallery': 'com.google.android.apps.photos',
    'whatsapp': 'com.whatsapp',
    'spotify': 'com.spotify.music',
    'clock': 'com.android.deskclock',
    'uhr': 'com.android.deskclock',
    'wecker': 'com.android.deskclock',
  };

```

I can’t see the call site that uses `knownApps`, but your comment mentions a `lower.contains(alias)` check. To implement “word‑boundary‑style” matching (especially for entries like `"whatsapp"`), you should:

1. Locate the loop where `knownApps` is used, something like:
   ```dart
   for (final entry in knownApps.entries) {
     if (lower.contains(entry.key)) {
       return entry.value;
     }
   }
   ```
2. Replace the `contains` check with a small helper that uses a `RegExp` with word boundaries for alphabetic aliases, falling back to `contains` when needed. For example:
   ```dart
   bool _matchesAlias(String lowerUtterance, String alias) {
     // Use word boundaries for simple alphabetic aliases (e.g. "whatsapp", "spotify").
     final isAlphabetic = RegExp(r'^[a-z]+(?:\s+[a-z]+)*$').hasMatch(alias);
     if (isAlphabetic) {
       final pattern = RegExp('\\b${RegExp.escape(alias)}\\b', caseSensitive: false);
       return pattern.hasMatch(lowerUtterance);
     }
     // Fallback: plain substring match for aliases with non‑letters / special cases.
     return lowerUtterance.contains(alias.toLowerCase());
   }
   ```
3. Then update the loop to use the helper:
   ```dart
   for (final entry in knownApps.entries) {
     if (_matchesAlias(lower, entry.key)) {
       return entry.value;
     }
   }
   ```

This preserves the current behavior for most aliases while reducing accidental matches for names like `"whatsapp"` and `"spotify"`. You may want to place `_matchesAlias` near the other regex helpers in `open_app_intent_parser.dart`.
</issue_to_address>

### Comment 3
<location path="lib/utils/open_app_intent_parser.dart" line_range="11-8" />
<code_context>
+    caseSensitive: false,
+  );
+
+  static final RegExp _machAufIntent = RegExp(
+    r'\bmach(?:en)?\s+\S+\s+auf\b',
+    caseSensitive: false,
+  );
+
</code_context>
<issue_to_address>
**suggestion:** The `mach ... auf` pattern misses some natural German phrasings.

This regex won’t match phrases with extra words between `mach` and `auf`, e.g. "mach mal YouTube auf" or "mach bitte die Kamera auf". To support these, consider allowing a limited span between them, e.g. `\bmach(?:en)?\b[\s\w]{0,20}\bauf\b`, or a similar bounded pattern to avoid over‑matching.
</issue_to_address>

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.

@@ -1578,6 +1579,29 @@ class ModelOrchestrator {
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Open-app shortcut currently bypasses any confirmation or disambiguation, which may be surprising on ambiguous queries.

Because this shortcut fires on any query that parses as both an open intent and a known alias, it will also trigger on informational prompts like “Explain how to open YouTube settings”, issuing an open_app call instead of letting the model respond. Consider tightening the heuristic (e.g., require the intent and alias to be close/within the same clause, or skip obvious “how to”/explanatory phrasing) so purely informational queries don’t invoke the tool.

Comment on lines +17 to +26
static const Map<String, String> knownApps = {
'youtube': 'com.google.android.youtube',
'yt': 'com.google.android.youtube',
'settings': 'com.android.settings',
'einstellungen': 'com.android.settings',
'chrome': 'com.android.chrome',
'maps': 'com.google.android.apps.maps',
'google maps': 'com.google.android.apps.maps',
'kamera': 'com.android.camera',
'camera': 'com.android.camera',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion: Review alias list and matching strategy for coverage and accidental matches.

Two small follow‑ups:

  • Confirm whether 'galeria' is intentional; if so, consider also adding 'galerie'.
  • lower.contains(alias) may match aliases as substrings inside longer words. For some entries (e.g. WhatsApp), consider using word‑boundary‑style matching (like \bwhatsapp\b) to reduce accidental matches without much added complexity.

Suggested implementation:

  /// Common app aliases → package names.
  ///
  /// Note:
  /// - `"galeria"` is intentionally kept as a Spanish/Portuguese variant.
  /// - `"galerie"` is added as the German variant.
  static const Map<String, String> knownApps = {
    'youtube': 'com.google.android.youtube',
    'yt': 'com.google.android.youtube',
    'settings': 'com.android.settings',
    'einstellungen': 'com.android.settings',
    'chrome': 'com.android.chrome',
    'maps': 'com.google.android.apps.maps',
    'google maps': 'com.google.android.apps.maps',
    'kamera': 'com.android.camera',
    'camera': 'com.android.camera',
    'photos': 'com.google.android.apps.photos',
    'galeria': 'com.google.android.apps.photos',
    'galerie': 'com.google.android.apps.photos',
    'gallery': 'com.google.android.apps.photos',
    'whatsapp': 'com.whatsapp',
    'spotify': 'com.spotify.music',
    'clock': 'com.android.deskclock',
    'uhr': 'com.android.deskclock',
    'wecker': 'com.android.deskclock',
  };

I can’t see the call site that uses knownApps, but your comment mentions a lower.contains(alias) check. To implement “word‑boundary‑style” matching (especially for entries like "whatsapp"), you should:

  1. Locate the loop where knownApps is used, something like:
    for (final entry in knownApps.entries) {
      if (lower.contains(entry.key)) {
        return entry.value;
      }
    }
  2. Replace the contains check with a small helper that uses a RegExp with word boundaries for alphabetic aliases, falling back to contains when needed. For example:
    bool _matchesAlias(String lowerUtterance, String alias) {
      // Use word boundaries for simple alphabetic aliases (e.g. "whatsapp", "spotify").
      final isAlphabetic = RegExp(r'^[a-z]+(?:\s+[a-z]+)*$').hasMatch(alias);
      if (isAlphabetic) {
        final pattern = RegExp('\\b${RegExp.escape(alias)}\\b', caseSensitive: false);
        return pattern.hasMatch(lowerUtterance);
      }
      // Fallback: plain substring match for aliases with non‑letters / special cases.
      return lowerUtterance.contains(alias.toLowerCase());
    }
  3. Then update the loop to use the helper:
    for (final entry in knownApps.entries) {
      if (_matchesAlias(lower, entry.key)) {
        return entry.value;
      }
    }

This preserves the current behavior for most aliases while reducing accidental matches for names like "whatsapp" and "spotify". You may want to place _matchesAlias near the other regex helpers in open_app_intent_parser.dart.

/// ASCII open verbs (safe with `\b`). Umlaut forms use normalized matching.
static final RegExp _asciiOpenIntent = RegExp(
r'\b(open|launch|start|starte|oeffne|oeffnen|aufmachen)\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.

suggestion: The mach ... auf pattern misses some natural German phrasings.

This regex won’t match phrases with extra words between mach and auf, e.g. "mach mal YouTube auf" or "mach bitte die Kamera auf". To support these, consider allowing a limited span between them, e.g. \bmach(?:en)?\b[\s\w]{0,20}\bauf\b, or a similar bounded pattern to avoid over‑matching.

@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 introduces a deterministic open-app shortcut parser supporting English and German, extends the alarm parser to support German queries, and improves voice input handling to support partial transcriptions and prevent premature message sending. The review feedback suggests refining the open-app parser by using word boundaries to avoid false substring matches, handling negated queries, and adding the German 'galerie' alias. Additionally, it recommends dynamically building the device tools capability prompt in the orchestrator to prevent the model from hallucinating unavailable tools.

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 +64 to +68
for (final alias in aliases) {
if (lower.contains(alias)) {
return knownApps[alias];
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Using lower.contains(alias) performs a substring match rather than a whole-word match. This leads to critical false positives where common words containing the alias as a substring trigger the app opening (e.g., "Python" matching "yt" and opening YouTube, or "Ruhr" matching "uhr" and opening the Clock app). We should use a regular expression with word boundaries (\b) to ensure we only match whole words.

Suggested change
for (final alias in aliases) {
if (lower.contains(alias)) {
return knownApps[alias];
}
}
for (final alias in aliases) {
final pattern = RegExp(r'\b' + RegExp.escape(alias) + r'\b');
if (pattern.hasMatch(lower)) {
return knownApps[alias];
}
}

Comment on lines +57 to +58
static String? tryParsePackage(String query) {
if (!_hasOpenIntent(query)) 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.

high

The deterministic open-app shortcut currently matches queries even if they contain negations (e.g., "Don't open YouTube" or "öffne YouTube nicht"). This results in the app being opened when the user explicitly requested the opposite. We should add a negation check to return null immediately if negation keywords are detected.

Suggested change
static String? tryParsePackage(String query) {
if (!_hasOpenIntent(query)) return null;
static String? tryParsePackage(String query) {
final negationPattern = RegExp(
r"\b(don\s*'?\s*t|not|no|nicht|kein|keine|nie|niemals|verhindern|stoppen|stop)\b",
caseSensitive: false,
);
if (negationPattern.hasMatch(query)) return null;
if (!_hasOpenIntent(query)) return null;

Comment on lines +28 to +29
'galeria': 'com.google.android.apps.photos',
'gallery': 'com.google.android.apps.photos',

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 German word for gallery is "Galerie", but it is currently missing from knownApps (which only has "galeria" and "gallery"). Adding "galerie" ensures German users can open the photos app using their native term.

Suggested change
'galeria': 'com.google.android.apps.photos',
'gallery': 'com.google.android.apps.photos',
'galeria': 'com.google.android.apps.photos',
'galerie': 'com.google.android.apps.photos',
'gallery': 'com.google.android.apps.photos',

Comment on lines +2383 to +2400
String _deviceToolsCapabilitySuffix(List<Tool>? textTools) {
final names = (textTools ?? const <Tool>[])
.map((t) => t.name)
.where((n) => n.isNotEmpty)
.toList();
final listed = names.isEmpty
? 'get_time, set_alarm, cancel_alarm, open_app, open_settings, search_web'
: names.join(', ');

return ' You control this Android device via tools: $listed. '
'When the user asks to open an app, set/cancel an alarm, open settings, '
'or search the web, call the tool immediately — do not say you lack '
'device access. '
'open_app packages: com.google.android.youtube, com.android.settings, '
'com.android.chrome. '
'If native function calling is unavailable, reply with only JSON like '
'{"name":"open_app","arguments":{"package":"com.google.android.youtube"}}.';
}

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 capability suffix prompt contains hardcoded instructions telling the model to call specific tools (like open_app, set_alarm, etc.) even if those tools are not actually present in the textTools list. This can cause the model to hallucinate or attempt to call unavailable tools. We should dynamically build the instructions based on the tools actually present in textTools.

  String _deviceToolsCapabilitySuffix(List<Tool>? textTools) {
    final names = (textTools ?? const <Tool>[])
        .map((t) => t.name)
        .where((n) => n.isNotEmpty)
        .toList();
    final hasTools = names.isNotEmpty;
    final listed = hasTools
        ? names.join(', ')
        : 'get_time, set_alarm, cancel_alarm, open_app, open_settings, search_web';

    final buffer = StringBuffer(' You control this Android device via tools: $listed. ');
    
    final actions = <String>[];
    if (!hasTools || names.contains('open_app')) actions.add('open an app');
    if (!hasTools || names.contains('set_alarm') || names.contains('cancel_alarm')) actions.add('set/cancel an alarm');
    if (!hasTools || names.contains('open_settings')) actions.add('open settings');
    if (!hasTools || names.contains('search_web')) actions.add('search the web');

    if (actions.isNotEmpty) {
      final actionStr = actions.join(', ');
      buffer.write('When the user asks to $actionStr, call the tool immediately — do not say you lack device access. ');
    }

    if (!hasTools || names.contains('open_app')) {
      buffer.write('open_app packages: com.google.android.youtube, com.android.settings, com.android.chrome. ');
    }

    buffer.write('If native function calling is unavailable, reply with only JSON like '
        '\'{"name":"open_app","arguments":{"package":"com.google.android.youtube"}}\'.');

    return buffer.toString();
  }

// Engine may auto-stop after pauseFor — flush full transcript once.
final ended = status == 'done' || status == 'notListening';
if (ended && _isListening) {
unawaited(_finishListening());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: unawaited(_finishListening()) swallows exceptions from _speech.stop()

If _speech.stop() throws, the exception is silently dropped because of unawaited, making speech failures impossible to debug from logs.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jul 18, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
WARNING 1
Issue Details (click to expand)

WARNING

File Line Issue
lib/widgets/voice_input.dart 53 unawaited(_finishListening()) swallows exceptions from _speech.stop()
Files Reviewed (9 files)
  • lib/screens/assistant_screen.dart
  • lib/screens/assistant_screen_beginner.dart
  • lib/services/model_orchestrator.dart
  • lib/tools/tool_definitions.dart
  • lib/utils/alarm_time_parser.dart
  • lib/utils/open_app_intent_parser.dart
  • lib/widgets/voice_input.dart
  • test/utils/alarm_time_parser_test.dart
  • test/utils/open_app_intent_parser_test.dart

Fix these issues in Kilo Cloud


Reviewed by step-3.7-flash · Input: 172K · Output: 47.3K · Cached: 4.8M

@involvex
involvex merged commit fbde0cf 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