Skip to content

Conversation

@zatteo
Copy link
Member

@zatteo zatteo commented Dec 11, 2025

In some case, we want to be able with the result of the LLM to:

  • insert it in the composer at the cursor position
  • replace the relevant content in the composer : replace the current selection or replace the full content

Here I add a "replace" optional button and correctly manage "replace" and "insert" action in all cases.

Screenshot

Screenshot 2025-12-11 at 15 46 29

Summary by CodeRabbit

Release Notes

  • New Features
    • AI Scribe dialog now includes a Replace button option alongside Insert for direct text replacement
    • Improved editor selection and text insertion handling for more precise cursor positioning
    • Added multi-language support for Replace action (English, French, Russian, Vietnamese)

✏️ Tip: You can customize this high-level summary in your review settings.

Collapsing a selection means removing it and keep the cursor at the
end. It will be needed by the scribe for "Insert" action where we
want to insert a text at the cursor even if the user selected text.
If at AI scribe creation, we pass a onReplace callback, it will show
on "Replace" button next to "Insert" button.
By leveraging selection collapsing provided by JS in both composer, we can now differentiate two actions that will be used by the scribe:
- inserting: always insert at end of cursor
- replacing: replace the content (selection wide or editor wide)
If we open from selection: insert + replace (replace selection)
If open from bottom bar with content : insert + replace (replace everything)
If open from bottom bar with empty content : insert
@coderabbitai
Copy link

coderabbitai bot commented Dec 11, 2025

Walkthrough

This pull request implements a replace functionality for the AI Scribe feature across the codebase. It adds a new JavaScript constant for collapsing text selections and introduces cross-platform editor manipulation utilities (collapse, clear, insert, replace) in the composer mixin. The web editor widget is updated to collapse selections on initialization. Localization strings for a "Replace" button are added across multiple language files (English, French, Russian, Vietnamese). The AI Scribe dialogs and suggestions are enhanced to support an optional replace callback, enabling a replace button in the UI alongside the existing insert button.

Suggested reviewers

  • chibenwa

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and concisely captures the main changes: adding a 'replace' action and improving the 'insert' action in the AI Scribe feature, which aligns with the core purpose shown in the raw summary.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ai-scribe-replace-insert

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@zatteo zatteo changed the base branch from features/ai to ai-scribe-ui December 11, 2025 14:51
@github-actions
Copy link

This PR has been deployed to https://linagora.github.io/tmail-flutter/4201.

@tddang-linagora
Copy link
Collaborator

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Dec 18, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
lib/features/composer/presentation/mixin/ai_scribe_in_composer_mixin.dart (1)

53-67: Consider returning Future<void> from async methods.

Both onInsertTextCallback and onReplaceTextCallback are declared as void ... async, which is an anti-pattern in Dart. This means callers cannot await them or handle errors. While this may be intentional for fire-and-forget callback usage, it silently swallows any exceptions from the awaited operations.

If the callback typedef OnInsertTextCallback is constrained to void Function(String), this pattern is acceptable for callbacks, but any errors in collapseSelection() will be unobservable.

🔎 Consider this pattern for better error observability:
-  void onInsertTextCallback(String text) async {
+  Future<void> onInsertTextCallback(String text) async {
     await collapseSelection();
-
     insertTextInEditor(text);
   }

-  void onReplaceTextCallback(String text) async {
+  Future<void> onReplaceTextCallback(String text) async {
     final selection = editorTextSelection.value?.selectedText;
     if (selection == null || selection.isEmpty) {
       clearTextInEditor();
     }
-
     insertTextInEditor(text);
   }

Note: This would also require updating OnInsertTextCallback typedef if it currently constrains the return type to void.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f385d39 and 08b23dd.

📒 Files selected for processing (11)
  • core/lib/utils/html/html_utils.dart (1 hunks)
  • lib/features/composer/presentation/mixin/ai_scribe_in_composer_mixin.dart (4 hunks)
  • lib/features/composer/presentation/widgets/web/web_editor_widget.dart (1 hunks)
  • scribe/lib/scribe/ai/l10n/intl_en.arb (1 hunks)
  • scribe/lib/scribe/ai/l10n/intl_fr.arb (1 hunks)
  • scribe/lib/scribe/ai/l10n/intl_messages.arb (1 hunks)
  • scribe/lib/scribe/ai/l10n/intl_ru.arb (1 hunks)
  • scribe/lib/scribe/ai/l10n/intl_vi.arb (1 hunks)
  • scribe/lib/scribe/ai/localizations/scribe_localizations.dart (1 hunks)
  • scribe/lib/scribe/ai/presentation/widgets/ai_scribe.dart (2 hunks)
  • scribe/lib/scribe/ai/presentation/widgets/ai_scribe_suggestion.dart (3 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-12-09T09:36:45.349Z
Learnt from: dab246
Repo: linagora/tmail-flutter PR: 4194
File: lib/features/manage_account/presentation/manage_account_dashboard_controller.dart:174-176
Timestamp: 2025-12-09T09:36:45.349Z
Learning: In Dart/Flutter projects using GetX, do not wrap getBinding<T>() calls in try/catch since they return null when not found. Only wrap Get.find<T>() calls in try/catch because they throw if a dependency is unavailable. When a binding is optional, consider checking Get.isRegistered<T>() or handling the null/exception path gracefully instead of blindly catching, and document the expectation for failure modes where a dependency may not be registered.

Applied to files:

  • scribe/lib/scribe/ai/localizations/scribe_localizations.dart
  • scribe/lib/scribe/ai/presentation/widgets/ai_scribe.dart
  • lib/features/composer/presentation/widgets/web/web_editor_widget.dart
  • scribe/lib/scribe/ai/presentation/widgets/ai_scribe_suggestion.dart
  • core/lib/utils/html/html_utils.dart
  • lib/features/composer/presentation/mixin/ai_scribe_in_composer_mixin.dart
📚 Learning: 2025-12-09T12:47:45.861Z
Learnt from: zatteo
Repo: linagora/tmail-flutter PR: 4196
File: scribe/lib/scribe/ai/data/datasource_impl/ai_datasource_impl.dart:34-35
Timestamp: 2025-12-09T12:47:45.861Z
Learning: In the tmail-flutter repository, avoid suggesting replacing DioError with DioException unless there is a codebase-wide migration. This pattern applies to all Dart files; when reviewing, only propose a DioError-safe change (e.g., compatible error handling or conversion) unless a global migration is in place. Ensure consistency of DioError usage across modules and flag any deviations for a repo-wide decision.

Applied to files:

  • scribe/lib/scribe/ai/localizations/scribe_localizations.dart
  • scribe/lib/scribe/ai/presentation/widgets/ai_scribe.dart
  • lib/features/composer/presentation/widgets/web/web_editor_widget.dart
  • scribe/lib/scribe/ai/presentation/widgets/ai_scribe_suggestion.dart
  • core/lib/utils/html/html_utils.dart
  • lib/features/composer/presentation/mixin/ai_scribe_in_composer_mixin.dart
🔇 Additional comments (14)
scribe/lib/scribe/ai/l10n/intl_vi.arb (1)

116-122: LGTM!

The Vietnamese localization for the replace button follows the established pattern and structure used for other button translations in this file.

scribe/lib/scribe/ai/localizations/scribe_localizations.dart (1)

125-128: LGTM!

The replaceButton getter is implemented correctly and follows the same pattern as other localization getters in this file.

scribe/lib/scribe/ai/l10n/intl_fr.arb (1)

116-122: LGTM!

The French localization for the replace button is correct and follows the established pattern.

scribe/lib/scribe/ai/l10n/intl_en.arb (1)

116-122: LGTM!

The English localization entry for the replace button is properly structured and follows the existing pattern.

core/lib/utils/html/html_utils.dart (1)

114-122: LGTM!

The JavaScript snippet correctly uses the Selection API's collapseToEnd() method with proper null checking. The IIFE pattern and naming convention are consistent with other constants in this file.

lib/features/composer/presentation/widgets/web/web_editor_widget.dart (1)

188-191: LGTM!

The collapseSelectionToEnd script is properly integrated into the web editor initialization scripts, following the same pattern as other WebScript entries.

scribe/lib/scribe/ai/presentation/widgets/ai_scribe.dart (2)

21-21: LGTM!

The optional onReplaceText parameter correctly extends the API to support the replace functionality while maintaining backward compatibility.


128-131: LGTM!

The conditional onReplace callback is correctly implemented. The replace action is only wired up when onReplaceText is provided, and it properly invokes the callback and closes the dialog.

scribe/lib/scribe/ai/l10n/intl_messages.arb (1)

116-122: LGTM!

The new replaceButton localization entry follows the established pattern and is consistent with the existing insertButton entry structure.

scribe/lib/scribe/ai/presentation/widgets/ai_scribe_suggestion.dart (2)

16-27: LGTM!

The optional onReplace callback is correctly added using the same type as onInsert, maintaining consistency while allowing the replace functionality to be opt-in.


189-209: LGTM!

The footer correctly conditionally renders the replace button when onReplace is provided. The null-check guard at line 191 makes the force-unwrap at line 197 safe. The visual distinction between the transparent replace button and the styled insert button provides clear UX hierarchy.

lib/features/composer/presentation/mixin/ai_scribe_in_composer_mixin.dart (3)

44-50: LGTM!

The clearTextInEditor method correctly handles both platforms using null-safe optional chaining.


80-106: LGTM!

The dialog integrations correctly wire up the new callbacks:

  • showAIScribeMenuForFullText: Conditionally provides onReplaceText only when there's content to replace (fullText.isEmpty ? null).
  • showAIScribeMenuForSelectedText: Always provides onReplaceText since this path requires an existing selection (guarded at lines 93-95).

This logic properly aligns with the PR's intent for insert vs. replace behavior.


33-42: HtmlUtils.collapseSelectionToEnd property access verified.

Both .name and .script properties exist on HtmlUtils.collapseSelectionToEnd and are correctly used for their respective platforms. The implementation properly differentiates between web and mobile APIs.

"placeholders_order": [],
"placeholders": {}
},
"replaceButton": "заменить",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cd scribe && cat -n lib/scribe/ai/l10n/intl_ru.arb | head -150

Repository: linagora/tmail-flutter

Length of output: 3891


Fix capitalization for consistency with other button labels.

The Russian translation "заменить" is lowercase, while all other button and action labels in this file use capitalized first letters (e.g., "Вставить" on line 111). Change "заменить" to "Заменить" to match the established capitalization pattern.

🤖 Prompt for AI Agents
In scribe/lib/scribe/ai/l10n/intl_ru.arb around line 117, the value for
"replaceButton" is "заменить" (lowercase) which is inconsistent with other
button labels that use an initial capital letter; update the translation to
"Заменить" so the first letter is capitalized to match the file's established
capitalization pattern.

@hoangdat
Copy link
Member

also in #4203

@hoangdat hoangdat closed this Dec 18, 2025
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.

4 participants