Implement cursor-position merging of final transcription
epic-speech-to-text-input-user-interface-task-007 — Add the cursor-position-aware merge logic to TranscriptionPreviewField. When a final transcription result arrives, insert the confirmed text at the current cursor position in the existing field content, preserving all pre-existing text before and after the insertion point. Dismiss the preview area after merge. Handle edge cases: cursor at start, cursor at end, text selected (replace selection with transcription).
Acceptance Criteria
Technical Requirements
Execution Context
Tier 1 - 540 tasks
Can start after Tier 0 completes
Implementation Notes
Implement merge logic as a pure static method for testability: takes `TextEditingValue current`, `String finalText` and returns a new `TextEditingValue`. Inside: `final before = current.text.substring(0, current.selection.start); final after = current.text.substring(current.selection.end); final merged = before + finalText + trailingSpace + after; final newOffset = before.length + finalText.length + trailingSpace.length;` Then set `controller.value = TextEditingValue(text: merged, selection: TextSelection.collapsed(offset: newOffset))`. For undo support, if using Flutter 3.7+ UndoHistoryController, call `undoController.value = UndoHistoryValue(canUndo: true)` before overwriting — or simply rely on the fact that programmatic controller.value changes are tracked automatically in Flutter's undo stack since 3.3. Listen for final results using `ref.listen(transcriptionStateManagerProvider, (prev, next) { if (next is TranscriptionComplete) _mergeFinalText(next.text); })` in initState/didChangeDependencies with a ProviderSubscription.
Testing Requirements
Unit tests: Extract the merge logic into a pure function `mergeAtCursor(String existingText, TextSelection selection, String insertText) -> TextEditingValue` and test all five cursor scenarios: empty field, cursor at start, cursor at end, cursor mid-text, active selection. Assert output text and cursor position for each. Assert trailing space logic (space added when next char is a letter, not added when next char is punctuation or end of string). Widget tests: Wire TranscriptionPreviewField with a mock provider that emits a final result; assert TextEditingController.text and selection.baseOffset after merge.
Assert preview area is gone after merge. Assert undo restores pre-merge state. Manual tests: Dictate into a field with existing text at various cursor positions; verify no text loss. Test undo on both iOS (shake to undo) and Android.
Merging dictated text at the current cursor position in a TextField that already contains user-typed content is non-trivial in Flutter — TextEditingController cursor offsets can behave unexpectedly with IME composition, emoji, or RTL characters, potentially corrupting the user's existing notes.
Mitigation & Contingency
Mitigation: Implement the merge logic using TextEditingController.value replacement with explicit selection range calculation rather than direct text manipulation. Write targeted widget tests covering edge cases: cursor at start, cursor at end, cursor mid-word, existing content with emoji, and content that was modified during an active partial-results stream.
Contingency: If cursor-position merging proves too fragile for the initial release, scope the merge behaviour to always append dictated text at the end of the existing field content and add the cursor-position insertion as a follow-on task after the feature is in TestFlight with real user feedback.
VoiceOver on iOS and TalkBack on Android handle rapid sequential live region announcements differently. If recording start, partial-result, and recording-stop announcements arrive within a short window, they may queue, overlap, or be dropped, leaving screen reader users without critical state information.
Mitigation & Contingency
Mitigation: Implement announcement queuing in AccessibilityLiveRegionAnnouncer with a minimum inter-announcement delay and priority ordering (assertive recording start/stop always takes precedence over polite partial-result updates). Test announcement behaviour on physical iOS and Android devices with VoiceOver/TalkBack enabled as part of the acceptance test plan.
Contingency: If platform differences make reliable queuing impossible, reduce partial-result announcements to a single 'transcription updating' message with debouncing, preserving the critical start/stop announcements. Coordinate with the screen-reader-support feature team to leverage the existing SemanticsServiceFacade patterns already established in the codebase.
The DictationMicrophoneButton must integrate with the dynamic-field-renderer which generates form fields from org-specific schemas at runtime. If the renderer does not expose a stable field metadata API for dictation eligibility checks, the scope guard and button visibility logic will require invasive changes to the report form architecture.
Mitigation & Contingency
Mitigation: Coordinate with the post-session report feature team early in the epic to confirm that dynamic-field-renderer exposes a field metadata interface including field type and sensitivity flags. Add a dictation_eligible flag to the field schema that the renderer passes to DictationMicrophoneButton as a constructor parameter.
Contingency: If the renderer cannot be modified without breaking changes, implement dictation eligibility as a separate lookup against org-field-config-loader using the field key as the lookup identifier, bypassing the renderer integration and keeping the dictation components fully decoupled from the report form architecture.