Skip to content

fix: sticky models, Stop, output guards, tool calls, adult mode#8

Merged
involvex merged 5 commits into
mainfrom
fix/sticky-model-stop-output
Jul 19, 2026
Merged

fix: sticky models, Stop, output guards, tool calls, adult mode#8
involvex merged 5 commits into
mainfrom
fix/sticky-model-stop-output

Conversation

@involvex

Copy link
Copy Markdown
Owner

Summary

  • Keep manually selected/imported models sticky (no silent Gemma→SmolLM RAM fallback); honor keepModelWarm; persist custom model preference
  • Add Stop button during generation; hard-cap length + repetition abort
  • Re-enable Gemma 4 Android native tools (was chatTools=0); parse ChatML/google_search markup → search_web; strip tool noise from UI
  • Strengthen adult-mode system prompt (lead + snackbar); plain Text while streaming to avoid Markdown RangeError
  • Bump version to 0.4.1

Test plan

  • Pin Gemma 4 → send 2+ messages → stays Gemma (or clear RAM error, not silent SmolLM)
  • While generating → Stop cancels and shows Stopped
  • Ask to search the web → browser opens via search_web (no raw tool_call markup in chat)
  • Enable Adult mode → new message applies stronger prompt
  • Long/streaming replies do not red-screen on RangeError

Made with Cursor

involvex and others added 3 commits July 19, 2026 02:34
Pin preferred/custom models across history wipe and RAM gates, honor keep-warm idle, bump flutter_gemma to 1.3.1, and stop runaway streams via length/repetition limits plus a Stop control.

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

cursor Bot commented Jul 19, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@changeset-bot

changeset-bot Bot commented Jul 19, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 817d07c

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 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.

Sorry @involvex, your pull request is larger than the review limit of 150000 diff characters

@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 several enhancements and robustness fixes to the Nova on-device AI assistant. Key changes include implementing a runaway/looped output detector (GenerationSafety), a unified tool call parser (ToolCallParser) to handle both JSON and ChatML-ish markup, and a user-facing "Stop" button to abort model generation mid-stream. It also restructures the adult mode policy prompt to place instructions at the beginning of the system prompt for better model adherence, and resolves UI crashes during streaming by disabling text selection on active markdown blocks. Feedback on these changes highlights opportunities to improve the repetition detector to support arbitrary period lengths, refine the markdown streaming UI to preserve rich formatting, and fix a potential bug in the tool call parser where replacing single quotes breaks queries containing apostrophes. Additionally, the markup stripper can be optimized to handle unclosed tags during active streaming.

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 thread lib/utils/generation_safety.dart Outdated
Comment on lines +29 to +52
/// True when [text] ends with the same ~80-char block repeated ≥3 times.
static bool hasConsecutiveRepetition(
String text, {
int window = repetitionWindowChars,
int minRepeats = repetitionMinRepeats,
}) {
if (window < 16 || minRepeats < 2) return false;
final need = window * minRepeats;
if (text.length < need) return false;

final chunk = text.substring(text.length - window);
if (chunk.trim().isEmpty) return false;

var repeats = 1;
var end = text.length - window;
while (repeats < minRepeats && end >= window) {
final prev = text.substring(end - window, end);
if (prev != chunk) break;
repeats++;
end -= window;
}

return repeats >= minRepeats;
}

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 current implementation of hasConsecutiveRepetition only detects repetitions where the period of the repeated substring is a divisor of window (80). If a model repeats a phrase of a different length (e.g., 6 characters like 'orange' or 35 characters), the shifted windows will not match, and the repetition will go undetected. To fix this, we should detect consecutive repetitions of any period p up to window.

  static bool hasConsecutiveRepetition(
    String text, {
    int window = repetitionWindowChars,
    int minRepeats = repetitionMinRepeats,
  }) {
    if (window < 16 || minRepeats < 2) return false;
    for (int p = 1; p <= window; p++) {
      if (text.length < p * minRepeats) continue;
      final chunk = text.substring(text.length - p);
      if (chunk.trim().isEmpty) continue;
      var isRepeat = true;
      for (int i = 1; i < minRepeats; i++) {
        final prev = text.substring(text.length - p * (i + 1), text.length - p * i);
        if (prev != chunk) {
          isRepeat = false;
          break;
        }
      }
      if (isRepeat && p * minRepeats >= 15) {
        return true;
      }
    }
    return false;
  }

Comment on lines +91 to +103
static String stripMarkup(String text) {
var out = text;
out = out.replaceAll(
RegExp(
r'<\|?tool_call\|?>[\s\S]*?(?:</?\|?tool_call\|?>|<tool_call\|>)',
caseSensitive: false,
),
'',
);
out = out.replaceAll(
RegExp(r'call:\s*[a-zA-Z0-9_]+\s*\{[^{}]*\}', 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

During streaming, the model outputs the tool call incrementally. Since the closing tag </|tool_call|> or } is not yet generated, the current regular expressions will not match, causing the raw tool call markup to temporarily flicker in the chat UI.

We can improve this by matching unclosed tags/blocks up to the end of the string ($).

Suggested change
static String stripMarkup(String text) {
var out = text;
out = out.replaceAll(
RegExp(
r'<\|?tool_call\|?>[\s\S]*?(?:</?\|?tool_call\|?>|<tool_call\|>)',
caseSensitive: false,
),
'',
);
out = out.replaceAll(
RegExp(r'call:\s*[a-zA-Z0-9_]+\s*\{[^{}]*\}', caseSensitive: false),
'',
);
static String stripMarkup(String text) {
var out = text;
out = out.replaceAll(
RegExp(
r'<\|?tool_call\|?>[\s\S]*?(?:</?\|?tool_call\|?>|<tool_call\|>|$)',
caseSensitive: false,
),
'',
);
out = out.replaceAll(
RegExp(r'call:\s*[a-zA-Z0-9_]+\s*\{[^{}]*(?:\}|$)', caseSensitive: false),
'',
);

Comment thread lib/widgets/chat_bubble.dart Outdated
Comment on lines +137 to +175
// Selectable MarkdownBody during token streaming causes
// RangeError (selection past length) and
// `_dependents.isEmpty` InheritedWidget teardown crashes.
message.isStreaming
? Text(
key: ValueKey('stream-text-${message.id}'),
message.text,
style: TextStyle(
color: isUser
? Colors.white
: Colors.white.withValues(alpha: 0.92),
fontSize: 15,
height: 1.5,
),
)
: MarkdownBody(
key: ValueKey('md-${message.id}'),
data: message.text,
selectable: true,
styleSheet: MarkdownStyleSheet(
p: TextStyle(
color: isUser
? Colors.white
: Colors.white.withValues(alpha: 0.92),
fontSize: 15,
height: 1.5,
),
code: TextStyle(
backgroundColor: Colors.black26,
color: Colors.cyan[100],
fontFamily: 'monospace',
fontSize: 13,
),
codeblockDecoration: BoxDecoration(
color: Colors.black38,
borderRadius: BorderRadius.circular(8),
),
),
),

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

Instead of falling back to a plain Text widget during streaming (which loses all markdown formatting, lists, and code blocks), we can use MarkdownBody with selectable: false. The RangeError crash is entirely caused by selectable: true (due to SelectableText selection offsets getting out of sync with rapid text updates). Using MarkdownBody(selectable: false) during streaming preserves rich formatting in real-time, and it will safely switch to selectable: true once streaming completes.

                      MarkdownBody(
                        key: ValueKey('md-${message.id}-${message.isStreaming}'),
                        data: message.text,
                        selectable: !message.isStreaming,
                        styleSheet: MarkdownStyleSheet(
                          p: TextStyle(
                            color: isUser
                                ? Colors.white
                                : Colors.white.withValues(alpha: 0.92),
                            fontSize: 15,
                            height: 1.5,
                          ),
                          code: TextStyle(
                            backgroundColor: Colors.black26,
                            color: Colors.cyan[100],
                            fontFamily: 'monospace',
                            fontSize: 13,
                          ),
                          codeblockDecoration: BoxDecoration(
                            color: Colors.black38,
                            borderRadius: BorderRadius.circular(8),
                          ),
                        ),
                      ),

Comment thread lib/utils/tool_call_parser.dart Outdated
Comment on lines +164 to +166
final asJson = raw
.replaceAll(RegExp(r'<\|?"?\|>'), '"')
.replaceAll("'", '"');

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 a global replaceAll("'", '"') to convert single quotes to double quotes will break any user query or argument that contains a legitimate apostrophe (e.g., "today's weather" or "what's the time"), making the JSON invalid and causing parsing to fail. Consider using a more robust parsing strategy or relying on the heuristic fallback directly for single-quoted strings.

@involvex

Copy link
Copy Markdown
Owner Author

Addressed Gemini review: broader repetition periods, strip incomplete tool markup while streaming, MarkdownBody(selectable:false) during stream, no apostrophe→quote rewrite in loose arg parse.

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

kilo-code-bot Bot commented Jul 19, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

All previously raised findings (repetition period breadth, streaming tool-markup flicker, MarkdownBody(selectable:false) during stream, and the global apostrophe→quote rewrite breaking apostrophe queries) were addressed in commit 7502dfb308057156ebd937983f630a9d540c677f. Verified against current HEAD — those defects are resolved and not duplicated here.

Files Reviewed (13 files, substantive code)
  • lib/utils/generation_safety.dart - new runaway/repetition guard; period-agnostic detection verified
  • lib/utils/tool_call_parser.dart - JSON + ChatML parser; alias/arg normalization and streaming-safe stripMarkup verified
  • lib/services/model_orchestrator.dart - sticky model pinning, Stop button, safety stop, sanitization, adult-mode lead
  • lib/models/adult_mode_policy.dart - lead/suffix prompts
  • lib/widgets/chat_bubble.dart - MarkdownBody selectable:false during stream
  • lib/screens/settings_screen.dart - adult-mode snackbar
  • lib/screens/assistant_screen.dart - Stop button wiring
  • android/app/src/main/kotlin/dev/nova/assistant/ToolExecutor.kt - query from query/q/queries list
  • lib/services/model_manager.dart - preferred custom model persistence
  • pubspec.yaml - version bump + flutter_gemma ^1.3.1
  • test/utils/generation_safety_test.dart - tests
  • test/utils/tool_call_parser_test.dart - tests
  • test/models/adult_mode_policy_test.dart - tests
Previous Review Summary

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review

Status: No Issues Found | Recommendation: Merge

All previously raised findings (repetition period breadth, streaming tool-markup flicker, MarkdownBody(selectable:false) during stream, and the global apostrophe→quote rewrite breaking apostrophe queries) were addressed in commit 7502dfb308057156ebd937983f630a9d540c677f. Verified against current HEAD — those defects are resolved and not duplicated here.

Files Reviewed (13 files, substantive code)
  • lib/utils/generation_safety.dart - new runaway/repetition guard; period-agnostic detection verified
  • lib/utils/tool_call_parser.dart - JSON + ChatML parser; alias/arg normalization and streaming-safe stripMarkup verified
  • lib/services/model_orchestrator.dart - sticky model pinning, Stop button, safety stop, sanitization, adult-mode lead
  • lib/models/adult_mode_policy.dart - lead/suffix prompts
  • lib/widgets/chat_bubble.dart - MarkdownBody selectable:false during stream
  • lib/screens/settings_screen.dart - adult-mode snackbar
  • lib/screens/assistant_screen.dart - Stop button wiring
  • android/app/src/main/kotlin/dev/nova/assistant/ToolExecutor.kt - query from query/q/queries list
  • lib/services/model_manager.dart - preferred custom model persistence
  • pubspec.yaml - version bump + flutter_gemma ^1.3.1
  • test/utils/generation_safety_test.dart - tests
  • test/utils/tool_call_parser_test.dart - tests
  • test/models/adult_mode_policy_test.dart - tests

Reviewed by step-3.7-flash · Input: 170.5K · Output: 28.4K · Cached: 2.8M

Co-authored-by: Cursor <cursoragent@cursor.com>
@involvex
involvex merged commit e3347bc into main Jul 19, 2026
6 checks passed
@involvex
involvex deleted the fix/sticky-model-stop-output branch July 19, 2026 02:03
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