Skip to content

[feature] the card is for voice typing, and the record button listens - #397

Merged
Lanznx merged 1 commit into
mainfrom
voice-typing-card-and-level
Sep 19, 2026
Merged

Lanznx merged 1 commit into
mainfrom
voice-typing-card-and-level

Conversation

@YJack0000

Copy link
Copy Markdown
Contributor

Two changes that landed together because they are the same correction from the same device session: the Live Activity was doing too much, and the thing the user actually looks at while dictating was faking it.

They are one PR rather than two because they were developed in one working tree and interlock — the removal and the additions touch overlapping surfaces, and an intermediate state would not have built.

The meeting card is gone

It shipped in 1.15 and was removed after use. The founder was told twice that the Dynamic Island and the lock-screen card are two presentations of one Live Activity and cannot be separated — so dropping the island for meetings also drops the lock-screen card that motivated the whole feature — and reaffirmed. The island's constant presence cost more than the lock-screen card was worth for a recording he is sitting in front of.

Starting a meeting now makes a live card disappear rather than turn red. That is the behaviour, not a bug: yieldMicrophone() ends the dictation and closes the window, so both of derive's inputs go nil. The answer is written at derive, because that is where somebody chasing the disappearance will land.

The dictation card loses ⏹ and ✕

Deciding what happens to a transcript is not a choice to make on a surface you are glancing at with the phone face-up on a table. A dictation card is read, not operated; .widgetURL still opens Parley, where both decisions are one tap away.

Standby keeps its one button

A first pass removed this too, as a literal reading of "no buttons". That was wrong and it is restored. ⏹ and ✕ settled the fate of words the card deliberately does not show. Ending standby settles nothing about content — standby is precisely the state in which nothing is being recorded. It closes a microphone that is open.

And it is the surface that most needs to offer it: every place that announces a window has always also been a way to end one (the Settings picker, the Record tab's bar, the keyboard's chip), and this is the fourth and the only one reachable from a locked screen — exactly where someone who has just noticed an unexplained orange dot is looking.

The record button now listens

KeyboardRootView pulsed the record button on a fixed scaleEffect(animating ? 1.25 : 1) loop that had nothing to do with what was being said. That is the canned visualiser this project's visual rules exist to prevent — driven by real amplitude, flat in silence — and the founder spotted it on the device immediately.

The keyboard could not have done better: no amplitude crossed the App Group. So:

  • A seventh mailbox (dictation-level.json) carrying one normalised level plus a stamp. Not a field on Downlink: that mailbox's staleness is the liveness watchdog's only input, and a value that moves whether or not a word does would refresh it forever — which does not weaken the watchdog, it switches it off.
  • 12 Hz, which is the rate the measurement already arrives at (AudioCapture taps 4096 frames ≈ 85 ms), so a faster mailbox would mostly republish unchanged readings into a backgrounded process.
  • Reuses the RMS the in-app meter already computes, published from the main-actor hop that meter already pays for — not from the audio thread, where a file write costs buffers. Gated by the existing reportsLevel gate, which already exists to stop an idle window spending battery on a number nobody reads.
  • Silence is a value the app writes, not one the keyboard infers: every path that stops reporting forces a final silent write, and a stale reading reads as silence too, so nothing is ever left frozen mid-swell.
  • Smoothing is asymmetric — fast attack so the swell lands with the syllable, slow release so the gap between words is a settle rather than a strobe. The ripple is a lagged copy of the same smoothed value drawn further out, so a syllable pushes one ring and then the next; that is a wavefront without a timer. In silence there is no ripple at all — the view leaves the tree.

A haptic for the microphone being taken

Tapping iOS's own dictation key in the strip below the keyboard opens a recording session in another process, and Parley stops hearing anything. That key cannot be hidden — it is system-drawn over third-party keyboards from iPhone X onward and there is no public API to suppress it; the only switch is the user's global Settings › General › Keyboard › Enable Dictation.

So the least Parley can do is tell the hand, at the one moment the user is most likely to keep talking into nothing. A flat pair rather than a rise or a fall: every other beat in the file answers something the user did, and direction is how they say what. This one answers something done to them, so it has no direction to borrow.

Deliberately not done

No waveform on the card. It cannot be driven by real audio — that would need activity.update() landing several times a second from a backgrounded app, which is this feature's one unverified assumption and a battery cost even if it worked. What is left is a loop that moves whether or not anyone is speaking: a picture of listening rather than evidence of it, on the surface whose entire job is to be believable. The reasoning is written where someone would otherwise add one.

Verification

  • swift test --package-path ios/ParleyKit → 421 tests, 0 failures (1 pre-existing skip). Six new wire-format tests for the level mailbox, including staleness, the unstamped file, NaN/clamping, and one pinning the level off the downlink.
  • xcodegen generate + xcodebuild -scheme Parley -configuration **Release** -destination generic/platform=iOS → BUILD SUCCEEDED.
  • Both locales present for every catalog key that has localizations.
  • Not verified — say it plainly: nothing here has been seen or felt. No device, and a simulator cannot reach a real dictation. The swell depth, the ripple reading as a travelling wave rather than concentric breathing, the attack/release constants, and the new beat's distinguishability from the existing four are all arguments. The smoothing constants are the thing most likely to want a tweak on a real phone.

🤖 Generated with Claude Code

Two changes that landed together because they are the same correction from the
same device session: the Live Activity was doing too much, and the thing the
user actually looks at while dictating was faking it.

**The meeting card is gone.** It shipped in 1.15 and was removed after use. The
founder was told twice that the Dynamic Island and the lock-screen card are two
presentations of one Live Activity and cannot be separated — so dropping the
island for meetings drops the lock-screen card that motivated the feature — and
reaffirmed. The island's constant presence turned out to cost more than the
lock-screen card was worth for a recording he is sitting in front of. So
`MicActivityState` is down to two modes, `MeetingRecorder` stops driving the
card, and `MeetingControlChannel` and the stop intent go with it. The meeting
haptics added in #395 stay: they were never part of the card.

**The dictation card loses ⏹ and ✕.** Deciding what happens to a transcript is
not something to do on a surface you glance at with the phone face-up on a
table. A dictation card is read, not operated; tapping it opens Parley, where
both decisions are one tap away.

**Standby keeps its one button**, which is a different kind of decision rather
than a surviving exception. ⏹ and ✕ settled the fate of words the card
deliberately does not show. Ending standby settles nothing about content —
standby *is* the state where nothing is recorded — it closes an open microphone,
and every surface that announces a window has always also been a way to end one.
This is the fourth, and the only one reachable from a locked screen, which is
where someone who has just noticed an unexplained orange dot is looking.

**The record button now listens.** It pulsed on a fixed 1.25× loop that had
nothing to do with what was being said — the canned visualiser this project's
visual rules exist to prevent, and the founder spotted it immediately. The
keyboard could not have done better: no amplitude crossed the App Group. So
there is now a seventh mailbox carrying one normalised level at 12 Hz (the rate
the measurement already arrives at), reusing the RMS the in-app meter computes
and the `reportsLevel` gate that already keeps an idle window from spending
battery on a number nobody reads. Silence is a value the app writes rather than
a value the keyboard infers, a stale reading reads as silence, and the ripple is
a lagged copy of the smoothed level drawn further out — so a syllable pushes one
ring then the next, and in silence there is no ripple at all.

**And a haptic for the microphone being taken.** Tapping iOS's own dictation key
in the strip below the keyboard opens a recording session in another process and
Parley stops hearing anything. That key cannot be hidden — it is the system's,
and there is no public API — so the least Parley can do is tell the hand, at the
one moment the user is most likely to keep talking into nothing. A flat pair
rather than a rise or a fall: every other beat answers something the user did,
and this one answers something done to them.

Nothing here is verified on hardware. The swell depth, the ripple reading as a
travelling wave, the smoothing constants and the new beat's distinguishability
are arguments, not observations.
@github-actions

Copy link
Copy Markdown

✅ SonarQube Quality Gate passed — pathorsAI_parley

0 open issues on this PR.

@Lanznx
Lanznx merged commit 5a3da55 into main Sep 19, 2026
3 checks passed
Lanznx pushed a commit that referenced this pull request Sep 19, 2026
The 1.15 What's New was written before #397 and described the meeting Live
Activity as its headline — a card on the Lock Screen with the elapsed time and
a Stop button. That feature was removed after a device session. Submitting
copy that describes something a reviewer can go and fail to find is worse than
submitting no copy, and it would have been the first paragraph.

Rewritten around what actually ships: the card is voice typing's, the record
button follows the voice instead of pulsing on a loop, and the haptics include
the two new endings and the microphone being taken by the system. Both locales,
zh-Hant as a peer rather than a translation. Verified against `asc_submit`'s
parser at version 1.15 — both sections resolve, stop at the 1.14 heading, and
mention no meeting card.

Build goes to 30 in `project.yml` rather than being passed as a workflow input.
27 shipped nothing and 28 and 29 were both uploaded, so every tag push since has
begun with a run that fails at the version check before anyone remembers to
dispatch with an override. The repo should say what TestFlight already holds.

Co-authored-by: YJack0000 <jack@pathors.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants