diff --git a/KisaniCal/ContentView.swift b/KisaniCal/ContentView.swift index 7b4f03b..968b7f1 100644 --- a/KisaniCal/ContentView.swift +++ b/KisaniCal/ContentView.swift @@ -161,6 +161,15 @@ struct ContentView: View { .presentationDetents([.height(150)]) .presentationDragIndicator(.visible) } + #if DEBUG + .onAppear { + // Test hook: auto-open Quick Add (used to verify the voice→field binding + // on the simulator, which has no microphone). Compiled out of Release. + if ProcessInfo.processInfo.environment["KISANI_AUTOADD"] == "1" { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { showQuickAddTask = true } + } + } + #endif .onChange(of: showWorkoutTab) { _ in ShortcutHandler.registerShortcuts() } } diff --git a/KisaniCal/ISSUES.md b/KisaniCal/ISSUES.md index 40fce71..d1d1070 100644 --- a/KisaniCal/ISSUES.md +++ b/KisaniCal/ISSUES.md @@ -1193,3 +1193,25 @@ Files: `SpeechRecognizer.swift` (new), `TodayView.swift`, `project.yml`. - Recognition locale is `.current`; no manual language picker. - Contains `print("[VOICE] …")` debug logging — harmless, could be removed before release. **Minor.** + +### Follow-up fix — transcript never appeared in the field +**Status:** Fixed (simulator-verified) + +The mic captured speech and the transcript reached `rawText`, but the text never +showed in the Quick Add field. Root cause: `NLTaskField.updateUIView` +(`UIViewRepresentable` over `UITextView`) had `guard coordinator.isEditing else { +…return }` — so when the field wasn't focused (the normal case when tapping the +mic), externally-set text (voice) was dropped and the placeholder stayed. Fixed +to render non-empty `text` even when not editing (caret moved to end). Verified on +simulator by pushing text through the real `speech.transcript` path: "Buy milk +tomorrow" → field shows it, date chip = Tomorrow; "Call Romeo at 11 AM" → field +shows it, chip = Today 11:00. + +Not exercised on the sim (no microphone / no UI-tap automation): live mic capture +and the literal Save tap. The save path is the same `submit()`/`addTask` used by +all typed tasks, so it's covered by normal task creation. + +Added DEBUG-only test hooks (`KISANI_AUTOADD`, `KISANI_VOICE_SIM`) to inject a +transcript on the simulator; `#if DEBUG`, compiled out of Release. + +Files: `TodayView.swift` (`NLTaskField.updateUIView`), `ContentView.swift`. diff --git a/KisaniCal/Views/TodayView.swift b/KisaniCal/Views/TodayView.swift index 73b3e1c..c28444c 100644 --- a/KisaniCal/Views/TodayView.swift +++ b/KisaniCal/Views/TodayView.swift @@ -1767,13 +1767,21 @@ struct NLTaskField: UIViewRepresentable { } func updateUIView(_ tv: UITextView, context: Context) { - guard context.coordinator.isEditing else { - if text.isEmpty { + let editing = context.coordinator.isEditing + + // Not focused: show placeholder when empty, but still render text that was + // set programmatically (e.g. voice dictation) so it actually appears. + if !editing && text.isEmpty { + if tv.text != placeholder || tv.textColor != .placeholderText { tv.text = placeholder tv.textColor = .placeholderText } return } + // Avoid clobbering the live UITextView (and the cursor) when the displayed + // text already matches the binding. + if tv.textColor == .label && tv.text == text && editing { return } + let sel = tv.selectedRange let attr = NSMutableAttributedString(string: text) let full = NSRange(location: 0, length: attr.length) @@ -1785,7 +1793,9 @@ struct NLTaskField: UIViewRepresentable { ], range: r) } tv.attributedText = attr - tv.selectedRange = sel + // Only restore the caret while the user is actively editing; for an external + // (voice) update, place the caret at the end. + tv.selectedRange = editing ? sel : NSRange(location: attr.length, length: 0) } func makeCoordinator() -> Coordinator { Coordinator(self) } @@ -1862,6 +1872,15 @@ struct AddTaskSheet: View { rawText = text print("[VOICE] Transcript bound to task field") } + #if DEBUG + .onAppear { + // Test hook: simulate a finished dictation by pushing text through the + // real speech.transcript path (the sim has no mic). Compiled out of Release. + if let t = ProcessInfo.processInfo.environment["KISANI_VOICE_SIM"], !t.isEmpty { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { speech.transcript = t } + } + } + #endif .padding(.horizontal, 16) .padding(.top, 14) .padding(.bottom, 10)