[feature] the keyboard can throw a dictation away instead of having to finish it - #326
Merged
Conversation
…o finish it The voice pane's only way out of a live session was ⏹, and ⏹ means deliver: the app drains the relay, folds in the last partial, spends a cloud polish round trip, and the keyboard pastes the whole transcript. A false start or a changed mind still had to land in the user's document before it could be deleted by hand — on this pane, by holding ⌫ through a paragraph. The words are visible above the record button while they are being spoken, so the pane let someone watch a mistake happen and do nothing about it. ✕ takes the deck's top-left slot while a session is live, one disc from ⏹. It writes `stopRequested` with `cancelRequested` beside it; the app cuts the relay instead of finishing it (no drain, no polish request), clears the transcript, and publishes `cancelled` — a third terminal state the keyboard never inserts from. The microphone goes back to the window the user chose, exactly as after ⏹, because nothing failed. The two can cross in flight: ✕ stays reachable through `finishing`, which is up to six seconds of polish during which the pane still reads as live. Both sides are made safe rather than merely unlikely — the app drops a polish result unless the session is still `finishing`, and the keyboard remembers the id it cancelled and refuses every later downlink for it. `cancelRequested` is optional on the wire so an uplink written by the previous build still decodes; a mailbox that fails to decode reads as "nothing there", which here would be a stop the app never hears. Relay segments now also stop rebuilding the transcript once a session is no longer active, so a socket that is still dying cannot put the discarded words back into a `cancelled` downlink. Closes #325
✅ SonarQube Quality Gate passed — pathorsAI_parley0 open issues on this PR. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
The voice keyboard gets a ✕ beside ⏹, shown only while a dictation is
running. It ends the session and throws the transcript away: nothing is
inserted, the host's field is left exactly as it was, and the microphone goes
back to the window the user chose.
Why
Closes #325.
⏹ is not a way out — it means deliver. A false start, a wrong word, someone
interrupting: the sentence still had to be transcribed into the document
before it could be deleted by hand, which on this pane means holding ⌫ through
a paragraph. The keyboard already shows the words live above the record
button, so until now the pane let the user watch a mistake happen and do
nothing about it. It also spent what it did not need to: a session nobody
wanted still paid for the relay drain and a cloud polish request.
How it works
✕writes the existingstopRequestedwith a newcancelRequestedbesideit. The app cuts the relay instead of finishing it — no drain, no polish —
clears the transcript, and publishes
cancelled, a third terminalDownlink.State.Three endings rather than two plus a special case:
donedelivers,errorexplains,
cancelledsays nothing. The keyboard's rule fordoneis "insertwhat is here", so a session where nobody spoke and a session the user threw
away have to be different events — otherwise the rule needs an exception, and
the exception is the bug.
cancel()is notstop()with the text blanked afterwards.stopis thedelivery path: it drains the relay for one more utterance, folds the partial
in, and spends up to six seconds polishing text that is about to be discarded
— with the drain standing as a network wait between the user's finger and the
microphone going quiet. It is not
fail()either: nothing went wrong, so themicrophone window survives and the next tap is still served in place, instead
of an error's "microphone visibly off".
The race, closed on both sides
✕ stays reachable through
finishing(the polish round trip), which is wheredonegets published — so the two can cross in flight. Both halves are neededbecause the app may answer slowly, or, killed, not at all:
finishing, and a cancelled one iscancelled.cancelledSessionremembers the id and refuses every laterdownlink for it, so a
donepublished a beat before the tap can never bedrained and pasted afterwards.
While in here, relay segments now also stop rebuilding the transcript once a
session is no longer
active— the same rule the two ending branches besidethem already followed. Without it a segment from a socket that is still dying
could put the discarded words straight back into a
cancelleddownlink.Wire compatibility
cancelRequestedisBool?on purpose: a synthesizedinit(from:)requiresevery non-optional key, and a mailbox that fails to decode reads as "nothing
there" — which for the uplink would mean a stop the app never hears. An uplink
left behind by the previous build has to keep working. There is a test for
exactly that.
Placement
Top-left was the slot the pane deliberately kept empty ("where the pane
breathes"), which is what lets ✕ arrive without the deck reflowing: the two
ways out of a dictation end up at the same height, one disc apart, and nothing
else moves. On devices that draw their own globe,
@is in that slot and asession borrows it —
@is a shortcut for something nobody is doing in themiddle of speaking. The globe below is never touched.
Drawn as an ordinary control disc, not in the recording red: the pane keeps
its one colour on the record button, which is already red during a session,
and a second red disc would read as the more dangerous of the two rather than
the smaller one. It gets a third haptic —
.rigid, on the press — because itis a third outcome; the success pattern would celebrate a delivery that did
not happen, and
.warningwould say something went wrong when nothing did.How it was verified
The desktop checklist below does not apply — this is
ios/only, whichci.ymlexcludes from CI, and this machine has no Xcode (Command Line Tools only,
no simulator runtimes), so the app and keyboard targets could not be compiled
or run here. Stating that plainly rather than ticking boxes:
ParleyKitbuilds (swift build) — the sharedDictationChannelchangeParleyKit,via a scratch executable (XCTest itself needs Xcode): every
Downlink.Stateround trips includingcancelled;cancelledisdistinct from
done/error; a ✕ uplink carries both flags; a ⏹ uplinkreads as not-a-cancel; an uplink written without
cancelRequestedstill decodes. All pass. The same assertions are committed as XCTest
cases in
DictationChannelTestsswiftc -frontend -parseclean on all eight changed Swift filesenandzh-Hant(
Keyboard/Localizable.xcstrings,App/Parley/Localizable.xcstrings)docs/design/ios-voice-keyboard.mdupdated: the third ending, the deckdiagram, the uplink mailbox
xcodebuild/ simulator run / device run. The twoexhaustive
switches overDownlink.State(DictationView.statusTitle,KeyboardViewController.drainDownlink) both got their new case, so thecompiler should be satisfied, but that is reasoning rather than a build.
Someone with Xcode should build once and try ✕ mid-sentence before this
ships in a TestFlight build.
Screenshots
None — see above; the keyboard could not be run on this machine. The layout
change is one disc appearing in the previously-empty top-left slot, drawn with
the existing
ControlDiscat the existingKBMetrics.roundKeysize.