Skip to content

iOS parity round 3: voice rounds, the free round timer with a Live Activity, and practice tools - #262

Merged
Programcode merged 11 commits into
feat/native-iosfrom
native-ios/parity-r3-practice
Oct 29, 2026
Merged

Programcode merged 11 commits into
feat/native-iosfrom
native-ios/parity-r3-practice

Conversation

@Programcode

@Programcode Programcode commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

Round 3 of iOS parity (plan: PARITY.md, section "Round 3"). It brings the website's practice tools to the app: voice rounds and dictation, the free round timer with a Lock Screen clock, the Sparring scorecard as a PDF, saved Crossfire rounds, and the speech time calculator and motion generator.

What changed

Voice rounds (Sparring and Crossfire). The student speaks each turn through Apple's speech recognition (expo-speech-recognition). The words fill the draft and can be corrected. On submit, the turn's speaking time, words per minute, fillers and long pauses are measured with the website's shared delivery code. speech-capture.ts ports the website's capture hook: it restarts when iOS ends a request, gives up after three quick failures, and waits 1.5 s on stop for the last words. Turns send voice: true and play the opponent's signed ElevenLabs segments from the existing voice function with expo-audio, falling back to the iOS voice. The first-use privacy notice keeps the website's wording, with Apple's recognizer in place of the browser. The scorecard and the Crossfire debrief show the measured delivery.

Dictation. A microphone beside Generate's motion and in Ask's composer.

Timer. It is now the website's free round timer, from packages/core/src/free-tools, which moved there from src/lib/free-tools with re-exports, so the website's imports are unchanged. It has seven formats (BP, WSDC, AP, PF, LD, Policy, school), POI windows with the knock, prep banks for both sides, overtime and grace, and custom formats saved on the device. The round persists, and its signals run from the root layout, so a speech keeps running after the timer closes. With an App Group, the development build also shows the clock as a Live Activity (Lock Screen and Dynamic Island) and a lock-screen widget. iOS draws both from SwiftUI timer texts, so they count without the app. This resolves TODO(live-activity).

Sparring PDF. The scorecard's menu exports the website's PDF, plus each spoken speech's measured delivery.

Saved Crossfire rounds open read only from History and the Debate tab, with an offline copy.

Practice gains the speech time calculator and the motion generator. "Prep both sides" opens New debate with the motion filled in.

Expo Go

Expo Go keeps working. The voice switch stays off with the existing "Coming with the app build" note, dictation buttons do not show, and nothing loads expo-speech-recognition, expo-widgets or @expo/ui at a file's top level. The config without DEBATELAB_BUILD is unchanged: npx expo config --json is identical before and after. expo-speech and @expo/ui (bumped to 57.0.26 to dedupe the copy expo-widgets brings) are in the SDK's module list.

Checks

  • npm run verify at the final head: exit 0 (docs, lint and knip, typecheck, 814 web tests, 30 collaboration tests, edge tests, build, service worker, 47 Chromium smoke tests). This is a local pass. GitHub Actions is billing-blocked, so its failures here are expected and say nothing about the code.
  • New unit tests: the speech capture state machine (restart, quick failures, stop grace, pause timing, refused microphone, one capture at a time), the stored timer session and the Live Activity times, and parseCustomFormat.
  • expo export --platform ios builds the Hermes bundle. Babel's 'widget' transform serializes both widget layouts.
  • DEBATELAB_BUILD=dev DEBATELAB_APP_GROUP=group.com.adam.debatelab expo prebuild --platform ios --no-install on Linux generates:
    • the ExpoWidgetsTarget extension with DebateTimerWidget and WidgetLiveActivity;
    • the App Group on both targets;
    • NSSupportsLiveActivities;
    • no aps-environment;
    • Hungarian and Spanish InfoPlist.strings.
  • Web-preview screenshots (iPhone and iPad, light and dark, English and Hungarian) through scripts/screenshots.ts, with new voice and tools groups:
    • The timer run checks the beep, double beep and knock, and the Lock Screen state.
    • The voice run uses a scripted recognizer. It checks that turns asked for the voice, that the opponent's replies went to the player, and that the filler and pause were counted.
    • The Sparring PDF is rendered from its HTML.
  • An Opus review of the diff found seven issues. All are fixed in the last commit, with a test for the shared recognizer. A Codex review could not run because the Codex sandbox fails to start on this VPS.

Bundle id, App Group and EAS

  • New ids. The app is now com.pistack.debateowl, and its widget extension is com.pistack.debateowl.ExpoWidgetsTarget. Both carry the App Group group.com.pistack.debateowl, which is the only value DEBATELAB_APP_GROUP accepts.
  • EAS project. eas init created @pi-stack/debateowl (project d60fc378-2e30-48b2-bcee-a1b1d1632bf9). The config has owner: 'pi-stack' and that project id.
  • Simulator profile. eas.json has a new development-simulator profile: an unsigned development client for the iOS Simulator, with the widget extension.
  • Simulator builds. Two attempts:
    1. b8e4ba30 failed after 7.8 minutes. In the Xcode phase that bundles the widget runtime, expo-widgets' metro.config.js makes the package its project root, and Expo CLI's resolver then cannot reach Metro's empty module in the workspace root's node_modules. I reproduced this on Linux. An eas-build-post-install hook (apps/mobile/scripts/patch-expo-widgets-bundle.mjs) gives that bundle an empty module inside the package. It fails loudly if a later expo-widgets changes the config it edits; expo-widgets 57.0.27 ships the same config.
    2. 5de7793f passed in 9.5 minutes wall time. Artifact (.tar.gz). It holds DebateOwl.app (com.pistack.debateowl, Live Activities on, the App Group, Hungarian and Spanish permission strings), PlugIns/ExpoWidgetsTarget.appex and the widget bundle.
  • Not run: the Simulator itself. The VPS is Linux, which has no iOS Simulator. The build proves the native code compiles and links; it does not prove the app runs.
  • Old id left on purpose. com.adam.debatelab survives only under supabase/, as in-app-purchase product ids (com.adam.debatelab.pro.monthly) and a test bundle constant. Those are App Store product ids and backend code, so this PR leaves them.

What still needs Adam

  1. Apple Developer account. Everything below depends on it.
  2. App Group. Register group.com.pistack.debateowl for both com.pistack.debateowl and com.pistack.debateowl.ExpoWidgetsTarget, with Live Activities enabled.
  3. A signed device build. The development profile needs DEBATELAB_APP_GROUP=group.com.pistack.debateowl in its env once the provisioning profile carries the group, following docs/development.md#build-day.
  4. Device test of what the web preview cannot prove:
    • voice rounds and dictation with the real recognizer (whether iOS continuous results arrive per utterance, as the code expects);
    • the audio handoff from recording to the opponent's voice;
    • ElevenLabs playback and the iOS-voice fallback;
    • the Live Activity, Dynamic Island and lock-screen widget (overtime turning red at the limit);
    • the PDF share sheet.

Not in this PR

  • Word export. I could not show that the docx package runs in Hermes: there is no Hermes runtime on this VPS. PDF only.
  • Backend. No changes under supabase/, no migrations, no deploys. The voice function is used as it is.
  • Merge with round 4. Round 4's Home Screen widget will add an entry to the same widgets[] array in app.config.ts. Expect a small merge there.

🤖 Generated with Claude Code

Programcode and others added 8 commits October 28, 2026 17:45
… core

The app's timer, speech-time calculator and motion generator need the same
logic as the website's free tools. Each old src/lib path re-exports the
core module; the custom format's URL codec stays in the website, and core
gains parseCustomFormat for formats kept on a device.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Timer now runs the free timer's formats (BP, WSDC, AP, PF, LD, Policy
and the school format), each speech's points-of-information window with
the knock, prep banks for both sides, overtime and grace, and custom
formats saved on the device. The round is kept in AsyncStorage and its
signals run from the root layout, so closing the timer leaves a speech
running.

With an App Group (DEBATELAB_APP_GROUP) the development build also shows
the clock as a Live Activity in the Dynamic Island and on the Lock Screen,
and as a lock-screen widget, drawn by iOS from SwiftUI timer texts. Expo
Go and builds without the group skip all of it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sparring and Crossfire get the website's voice rounds: the student speaks
each turn through Apple's speech recognition (expo-speech-recognition,
restarting when iOS ends a request, as the website's capture does), the
words fill the draft, and the turn's speaking time, pace, fillers and long
pauses are measured with the shared delivery code. The opponent's replies
play from the voice function's signed ElevenLabs segments with expo-audio,
falling back to the iOS voice. The first-use privacy notice keeps the
website's wording, with Apple in place of the browser. Generate's motion
and Ask's question take dictation.

Voice needs the development build; in Expo Go the switch stays off with
the existing note and the dictation buttons do not show.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The scorecard's menu exports the website's Sparring PDF (set-up, totals,
category scores and feedback, key moments, the transcript) through
expo-print, with each spoken speech's measured delivery. Word export is
left out: the docx package could not be shown to run in Hermes here.

A saved Crossfire round now opens from History and the Debate tab: the
debrief, the spoken delivery of a voice round and the transcript with its
signals, read from the crossfires row the website saves, with an offline
copy like every other read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both are the website's free tools, from the same core logic and the
website's tools catalog: the calculator counts a speech's words, times it
at four paces and shows which slots of the seven formats it fits; the
generator filters the practice motion bank by topic, level and type and
draws one motion or three. A drawn motion can be copied, shared or opened
as a new debate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A wide iPad's outline rail is a menu too, so the first menu item was the
rail's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Words heard after a Sparring Reset no longer start the reset turn's
  clock: dictated words append to the draft without the start path.
- One capture uses iOS's recognizer at a time; starting another cancels
  the first, so two screens never end or hear each other's sessions.
- An opponent clip that never reports its end gives up after its length
  plus slack, so the device voice reads the rest; clips fetched for a
  reply that stopped are deleted.
- Sending a spoken turn with nothing heard returns quietly.
- Generate's dictation keeps words that arrive between renders.
- A custom format saved before the stored list loads no longer replaces it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 28, 2026 19:11
@coderabbitai

coderabbitai Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing.

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The job was not started because recent GitHub Actions payments have failed or your spending limit needs to be increased.

Programcode and others added 3 commits October 28, 2026 19:19
…oject

The app is com.pistack.debateowl, its widget extension
com.pistack.debateowl.ExpoWidgetsTarget, and both carry the App Group
group.com.pistack.debateowl, the only value DEBATELAB_APP_GROUP accepts.
The config links the EAS project @pi-stack/debateowl that eas init
created, and eas.json gains a development-simulator profile: an unsigned
development client for the iOS Simulator, with the widget extension.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EAS build b8e4ba30 failed in the Xcode phase that bundles the widget
runtime: expo-widgets' metro.config.js makes the package its project root,
and Expo CLI's resolver then cannot reach Metro's empty module in the
workspace root's node_modules. An eas-build-post-install hook gives that
bundle an empty module inside the package; it fails loudly if a later
expo-widgets changes the config it edits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…colours

The live round passed no formatted number, so it read {{formatted}} words. The lock-screen widget now uses the timer screen colours for side B and both sides.
@Programcode
Programcode merged commit 3ad823c into feat/native-ios Oct 29, 2026
3 of 13 checks passed
@Programcode

Copy link
Copy Markdown
Owner Author

Replaced by #266, which combined rounds 3 and 4 and is now merged.

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