uttrflow / uttrflow/uttrflow-swift
Stopping a double-tap dictation from the menu bar or at the 4-minute cap leaves the dictation shortcut dead until relaunch
- Dominant language
- Swift
- Stars
- 4
- Forks
- 17
- Avg merge
- 3h 32m
- Merged PRs (30d)
- 277
Description
## What happens
`DictationController` remembers a double-tap (hands-free) dictation in `isHandsFree`, but only two paths ever clear it: a second double tap (`stopHandsFree`, `Sources/UttrflowPipeline/DictationController.swift:386-390`) and a change of activation mode (`adopt`, line 151). Every other way the dictation can end leaves it `true`:
- **Menu bar → Stop Dictation**, the clipboard panel's dictate button and the failure notice's Retry all go through `toggleFromControl` → `toggleListening` (lines 298-305), which calls `pipeline.finishRecording()` and never touches `isHandsFree` (`Sources/Uttrflow/AppDelegate.swift:1260-1261`, `:966`, `:1812`).
- **The 4-minute cap**: `finishAtTheLimit` (lines 334-338) finishes the recording and leaves `isHandsFree` set.
With `isHandsFree` stale and the microphone closed:
- a **hold** does nothing: `press` returns at `guard !isHandsFree` (lines 197-200) without opening the microphone, and the release returns at line 349 because nothing is listening;
- a **double tap** does nothing either: both taps hit the same guard, and `endHold` returns before it can count a tap, so the gesture that would clear the flag can never complete.
The shortcut stays dead for the rest of the launch, or until the user happens to switch the activation mode in Settings and back. Nothing is shown: no failure, no notice.
## Why it matters
The double tap exists so a long dictation needs no held key. Ending that dictation from the menu bar is the obvious way for anyone who does not remember that a second double tap is the only "correct" stop, and the cap ends every dictation that runs long. Both leave dictation silently broken, which reads exactly like the "works sometimes" reports #141 was about. P1: the core flow stops working until relaunch.
## How to reproduce (headless, no microphone)
Add to `Tests/UttrflowPipelineTests/DictationControllerTests.swift`, using the file's own `makeHarness` and `tap` helpers:
```swift
@Test func handsFreeStoppedByAClickLeavesTheHoldWorking() async {
let harness = makeHarness()
await tap(harness)
harness.clock.advance(by: .milliseconds(120))
await tap(harness) // hands-free, listening
await harness.controller.toggleFromControl() // menu bar: Stop Dictation
harness.clock.advance(by: .seconds(2))
await harness.controller.handle(.pressed)
#expect(await harness.pipeline.currentState.isListening) // fails on main
}
```
On main (26d7bc1) this fails, and so does a double tap afterwards. The same shape in `DictationLimitWiringTests` (double tap, advance the `ManualClock` past `warnAfter` then `stopAfter`, wait for the insertion, press) also fails: the hold after the cap does not open the microphone.
## Acceptance criteria
- However a hands-free dictation ends (second double tap, menu bar or panel control, Retry, the cap, a cancel), the controller returns to a state where the next hold and the next double tap both work.
- Tests cover at least the click stop and the cap stop, each followed by a hold and by a double tap.
- `Docs/pipeline-gestures.md` says how a hands-free dictation can be ended.
Contributor guide
Research direction
Start with Sources/UttrflowPipeline/DictationController.swift and the toggleFromControl and finishAtTheLimit paths, then review DictationControllerTests.swift and DictationLimitWiringTests for the existing harnesses. Run the named reproductions and verify that click stops and the four-minute cap leave both hold and double-tap gestures usable. Update Docs/pipeline-gestures.md to document the available ways to end hands-free dictation.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- macos, swift
- Domain
- desktop, documentation, testing
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100