Voice: show dictated transcript in the Quick Add field

The mic captured speech and the transcript reached rawText, but it never appeared
in the field. NLTaskField.updateUIView (UITextView representable) returned early
when the field wasn't focused (`guard coordinator.isEditing`), so externally-set
text (voice dictation, the normal not-focused case) was dropped and the
placeholder stayed.

Fix: render non-empty `text` even when not editing, placing the caret at the end;
keep the cursor-preserving fast path while actively editing. No new voice system —
this only fixes the existing mic button's transcript→field binding.

Verified on simulator via the real speech.transcript path: "Buy milk tomorrow" →
field shows it (chip: Tomorrow); "Call Romeo at 11 AM" → field shows it (chip:
Today 11:00). Adds DEBUG-only test hooks KISANI_AUTOADD / KISANI_VOICE_SIM
(compiled out of Release). Logged under KC-38.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
kutesir
2026-06-24 21:22:45 +03:00
parent f8d0a80f17
commit ad304913e4
3 changed files with 53 additions and 3 deletions

View File

@@ -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 voicefield 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() }
}

View File

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

View File

@@ -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)