Skip to content

iOS parity rounds 3+4 combined - #266

Merged
Programcode merged 52 commits into
feat/native-iosfrom
native-ios/parity-r3r4-combined
Oct 29, 2026
Merged

Programcode merged 52 commits into
feat/native-iosfrom
native-ios/parity-r3r4-combined

Conversation

@Programcode

@Programcode Programcode commented Oct 29, 2026 •

Copy link
Copy Markdown
Owner

Replaces #262 (round 3) and #265 (round 4), so both rounds can be reviewed and built together. Those two PRs stay open for reference; close them once this one is accepted.

This branch starts at #262's head (5f3e22f) and merges #265's head (346d6a2) with a normal merge commit. Both rounds are kept in full.

What's in it

How the conflicts were resolved

Twelve files conflicted. Where both sides added something (imports, locale keys, routes in AGENTS.md, the screenshot groups), both are kept.

  • Opponent turns in Sparring and Crossfire. In a voice round the opponent reads its turn aloud, as in round 3. Otherwise VoiceOver announces the finished turn, as in round 4. You never get both.
  • ⌘↩ calls round 3's async submit, so the microphone stops and the last spoken words reach the draft before the turn is sent.
  • Crossfire debrief. It shows round 4's celebration and also round 3's delivery summary.
  • Practice tab. Round 4's animated rows, with round 3's free tools named from the tools catalog.
  • _layout.tsx. Round 3's TimerRunner and round 4's KeyboardDoneBar are both mounted.
  • @expo/ui. Kept at round 3's ~57.0.23.
  • Locales. Joined. The export.* and generateResults.* keys that Generate writes cases the way school debaters prepare them #244 (case v2) removed stay removed; nothing in the app still uses them.

Follow-ups to the merge

Expo config of this build

I read the development-simulator config with expo config --type introspect (DEBATELAB_BUILD=dev, DEBATELAB_APP_GROUP=group.com.pistack.debateowl):

  • bundle com.pistack.debateowl, App Group group.com.pistack.debateowl, applinks:debateowl.com and applinks:www.debateowl.com;
  • a single ExpoWidgetsTarget extension (com.pistack.debateowl.ExpoWidgetsTarget, same group) carrying both DebateTimerWidget (round 3) and CurrentDebate (round 4);
  • NSSupportsLiveActivities, set by the expo-widgets Info.plist mod at prebuild.

Quick actions and Spotlight stay off, as in #265: they turn on only once expo-quick-actions or expo-core-spotlight is installed.

Verification

Local only, at head 3ad823c. NODE_OPTIONS=--max-old-space-size=3072 npm run verify exited 0: docs check, lint, typecheck (web, agentic, core, mobile app and scripts), 877 unit tests in 134 files (the mobile tests included), 30 collaboration tests, 553 Edge tests, the build with prerender and site checks, the service worker offline test, and 47 desktop Chromium @smoke tests. An earlier run died partway through the smoke tests when the VPS ran short of memory; the run reported here is a clean full re-run. No CI has run on this PR: every GitHub Actions job ends within 2 to 3 seconds with no logs, which means the account is out of Actions minutes, as on #262 and #265. Treat this as a local pass, not a CI pass. On the review fix (61e9953), I ran eslint on the file and the mobile tsc only.

Not run: verify:full, the database gate, WebKit and staging. This merge changes no migrations.

EAS build

One development-simulator build of head 3ad823c: b96a66c7. Finished in 8.5 minutes, on the first attempt. Download the simulator app (.tar.gz). I unpacked it to check: DebateOwl.app is com.pistack.debateowl, with NSSupportsLiveActivities and the App Group, and PlugIns/ExpoWidgetsTarget.appex (com.pistack.debateowl.ExpoWidgetsTarget) contains both widgets, DebateTimerWidget and CurrentDebate. The later commit 61e9953 changes JS only, so this binary still matches the head's native side.

Review

One Codex review round on 3ad823c found one issue, now fixed in 61e9953. Round 3's custom words-per-minute field in the speech time calculator was a plain TextInput, so it missed round 4's keyboard Done bar. A number pad has no Return key, so the keyboard could not be put away. That field was the last plain TextInput in the app.

Screenshots (web export, not the native app)

Taken with apps/mobile/scripts/screenshots.ts, signed in on staging, with model calls faked. Groups first-run, results, sidebar and sparring, on iPhone, iPad portrait and iPad landscape, in English light mode: 91 shots. They include Ollie's welcome and the tour, the case and its celebration, the Sparring celebration, and the iPad sidebar with Speech time calculator and Motion generator at the end of Practice.

The run exited 1, and not because of the merge. The script's outline-jump step looks for a "Case structure" heading that case v2 (#244) removed, so on a wide iPad it times out after the case is shot. The step is the same in the base, #262 and #265. One shot, iphone-light-en-sparring-your-turn, is listed in clipped.json: it is the opponent's speech preview, which is cut to a few lines on purpose.

EAS Simulator is not enabled for the pi-stack account, so no native screenshots were taken.

Needs Adam (carried over from #265, corrected for the new id)

  • The website's AASA file and its deploy. Serve https://debateowl.com/.well-known/apple-app-site-association (and the same on www) as application/json, with no redirect, and make sure vercel.json's SPA fallback doesn't catch it. Replace TEAMID with the team ID:
    {
      "applinks": {
        "details": [
          {
            "appIDs": ["TEAMID.com.pistack.debateowl"],
            "components": [
              { "/": "/share/*", "comment": "Shared results" },
              { "/": "/app/*", "comment": "App pages" }
            ]
          }
        ]
      }
    }
  • Apple Developer account. Register Associated Domains and the App Group group.com.pistack.debateowl for both com.pistack.debateowl and com.pistack.debateowl.ExpoWidgetsTarget, with Live Activities enabled.
  • The package decisions from iOS parity round 4: case v2, iPad sidebar, share links, offline queue, first run, accessibility and Duolingo-style motion #265 (expo-quick-actions, expo-core-spotlight@^2, a UIKeyCommand module) and the device tests from both PRs still apply.

🤖 Generated with Claude Code

Programcode and others added 30 commits October 28, 2026 07:32
* WIP: restructure generated debate cases

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

* Test the new case shape, old saved cases and every case export

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

* Type the PDF stand-in in the case export test

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

* Accept null for the new optional case fields

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

* Open the folded Researched sources panel before checking its links

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

* R2 backs up the team's arguments instead of adding new ones

R2 now follows Adam's own R2 speech (Debate print 1, 2025-03-26): who is
speaking, about 2 minutes backing up the team's earlier arguments
(reinforce), then the bulk in "If they say…" answers, and a closing line.
Length sets R2's answer count (5/6/8) instead of an argument count.

The prompt, case labels and exports no longer use framework, burden of
proof, weighing, warrant, impact, thesis or counterpoint. Old saved cases
keep those sections under plain names. Sparring's school-role guidance and
the agentic test context describe the same structure.

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

* Sparring's school-role guidance matches the new case structure in all three languages

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

* Hold R2 to the length of Adam's own R2 speech

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

* R2 answers each of their 2-3 arguments in full

The other side usually runs only 2-3 arguments, so R2's "If they say…"
holds one full answer per likely argument (about 90-120 words at standard
length), built the way Adam writes his own refutations in Debate print 1:
a direct reply, a fact with its source, what it means, a tie back to our
argument, and a closing question or firm line. He underlines the ones the
other side used and builds on them in the round.

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

* R2 answers push back first, as Adam's do, and keep to his length

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

* R2 prepares a fuller answer to everything they are likely to say

Adam asked for more "If they say…" blocks, each with more content, like
his own debates. R2 now writes 6 blocks at standard length (5 brief): their
main arguments, the points they use to back them up, and their answers to
our case. Each answer pushes back at once, gives at least two specific
facts with source and year, says what they mean, asks a question that
turns their point around, and ties back to our argument by name. Numbers
must come from the attached research and every cited source must be in
the sources list.

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

* Give a case room to reason so a long R2 is not cut off

Claude Haiku reasoned for 6,000-10,000 tokens on an R2 case at low
effort, past the 3,000 that a 15,000 max_tokens left it. One reply was
cut off and its retry finished at 128 s of the 130 s budget. Generate now
keeps at least 10,000 tokens for reasoning (max_tokens 22,000 at low) while
its reply cap stays at 12,000, so every cap still fits its retry. Also keep
R2's backing-up paragraphs short beside the longer answers.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Describe the new case format on the public guides

The guides still described the old case: thesis, framework, burden of
proof, contentions with warrants and impacts, and a predicted opponent
case. Generate now writes an opening, arguments (point, explanation,
evidence, why it matters), "If they say… we answer…" replies, a closing
line and sources (#244). The copy changes in seo_guides.py, seo_answers.py
and the PAGES meta in generate_seo_content.py, and the pages and llms.txt
are regenerated from them. The LD, PF, Policy and weighing format guides
are unchanged.

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

* Match the case guide to the shipped R2: no new arguments, five or six If they say blocks

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The staging project was renamed from debate-trainer-staging to DebateOwl
Staging, and the sync refused to run since 2026-10-28 11:18 UTC. The target
check now keys on the ref (brifeyypjauyusmkptkx), which never changes. The
SUPABASE_PROJECT_ID override check, the staging != production check and the
explicit --project-ref on every CLI call are unchanged.
The case summary cut backing-up paragraphs and why-we-win reasons to 240
characters and kept only 3 If-they-say answers at 200. They now pass in
full (2,000-character safety bound each, up to 8 answers). A case may
contribute 12,000 characters, and all saved results 16,000.
Main's #244 changed the website's copies of code that round 1 moved into
packages/core. The merge keeps the website's re-exports and ports main's
changes into core: the v2 live draft sections, the transform, the
regenerate merge (opening, reinforce, whyWeWin, closing, merged sources),
the R2 and Conclusor argument check (now hasExpectedArguments, shared by
both apps) and the flowchart built from readCase.

The app renders cases with readCase the way the website's GenerateResults
does: opening, arguments with explanation, evidence and why it matters,
backed-up arguments, why we win, "If they say… we answer…", the closing
line and sources, plus older cases' sections under their plain names. The
PDF, copied text, Discuss message and regenerate list follow it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 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>
A full-screen iPad window now shows the app's own sidebar beside the root
stack: the Current Debate switcher and New debate on top, then Debate
(Dashboard, History), Prepare (Generate, Attack, Brainstorm, Factoids,
Ask), Practice (Sparring, Crossfire, Flowchart, Timer), Library (Sources,
Paste a link, Write a note, Import a file) and You. Sections open and close
from their headers, which VoiceOver reads as headings with their expanded
state, and the closed ones are kept on the device. The row for the current
screen is highlighted, including a tool's saved documents; a closed section
holding it shows a dot. Rows grow with Dynamic Type and wrap to two lines;
the sidebar widens with the text size up to a cap.

The native tab bar hides while the sidebar shows. Split View, Slide Over,
Stage Manager windows and widths under 700 points keep the tab bar.
Screens lay out for the room beside the sidebar through ReservedWidth,
which sheets reset.

The screenshot script waits for either shell, opens tools from the
sidebar on iPad, gains a `sidebar` group with its checks, and can relay
Edge Function calls with an allowed Origin so it runs on any port.

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

The first run opens as one sheet: Ollie's welcome, line by line as on the
website (Skip animation, Reduce Motion shows it settled), then the
website's four tour steps as pages with Back, Next, Skip tour and Get
started, and Start debate on the first step before a debate exists. The
welcome opens once per phone and the tour once per account, under the
website's keys; swiping the sheet away counts as done.

The Debate tab shows the website's trial reminder ("Free trial: 2 days
left.") while a trial runs, with See plan and Hide; the day count moves
to packages/core so both apps share it.

When the server ends a session (a refused refresh, on launch or later),
the sign-in screen says "Your session expired. Sign in again." A sign-out
the student asks for, or Delete account, does not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s, text-size caps

- Every text field is AppTextInput, with one Done bar above the keyboard
  on iOS (InputAccessoryView, drawn once in the root layout), so a
  multi-line answer can always be put away.
- VoiceOver reads a finished Ask answer, Crossfire reply and Sparring
  speech once, as "Answer ready" and its first sentences, never per token;
  a finished tool result announces its title.
- Section titles in documents, Ask's answers, the claim panel, live drafts,
  empty states and Sparring cards are headings for the rotor.
- Text grows with Dynamic Type everywhere; only fixed chrome is capped:
  floating action-bar buttons and segmented controls at 1.6x, the sidebar
  at 1.35x, the preview's tab labels at 1.3x. The web preview can draw a
  text size (font-scale.web.ts) so the screenshots show it.
- iPad keyboard shortcuts through one hook: ⌘N new debate, ⌘K switcher,
  ⌘↩ sends in Ask, Crossfire and Sparring. They run in the web preview;
  the native app needs a UIKeyCommand module in the development build.

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 keyboard

Screen, DocumentScreen (both layouts) and the Flowchart's scroll views adjust
their insets for the keyboard, keep taps on buttons while it is up and put
it away when dragged, as the form sheets already do.

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>
Share links: a saved case, attack, brainstorm or factoids document has
Share link in its menu. It calls the existing `share` Edge Function with
the student's session and opens the share sheet; a link made this session
for the same unchanged result is reused, since every call makes a new one
and counts toward the daily limit. Links open in a read-only viewer
(share/[code]) through the public get_shared_item RPC, signed in or out,
with Open on the website when a link cannot be shown.

Universal links, app side: +native-intent.tsx maps
https://debateowl.com/share/<code> and /app/<page> (also www) to app routes
and sends any other web address to the Debate tab. The mapping is a
hand-written parser at a trust boundary and is tested against other hosts,
ports, user info, encoded segments and javascript: URLs.

Word export: the website's DOCX builders move to
packages/core/src/export/docx.ts unchanged (every part of the generated
files is identical before and after, in English and Hungarian), and the
app packs them with Packer.toBase64String and shares the .docx. The
`docx` package runs without Node or DOM globals and compiles with hermesc.

The screenshot script gains a `share` group that makes one real link on
staging and opens it in the viewer.

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

- A Home Screen widget (small and medium, expo-widgets with @expo/ui): the
  motion, sources, results and rounds, a Spar link, and Start a debate when
  there is none. The debate context and the Debate tab publish to it; in
  Expo Go and on the web that is a no-op.
- Quick actions (New debate, Start timer, Ask your coach) and Spotlight
  indexing are written behind nativeCapabilities() and switch on when
  expo-quick-actions or expo-core-spotlight is installed. Neither is
  installed: neither has been tried on SDK 57 on a device yet.
- The development build gets the Associated Domains entitlement for
  debateowl.com and www, and, with DEBATELAB_APP_GROUP, the CurrentDebate
  widget in the existing expo-widgets entry. Without DEBATELAB_BUILD the
  config is unchanged (compared with `npx expo config --json`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New notes, source edits (title, folder, text) and Active switches now go
through one queue kept on the phone under the account's user id. Online
it sends them in order as before. With no connection, or no answer from
the server, the change shows at once, stays in the Library's saved copies
with a "Waiting to sync" badge, and the offline notice counts what is
waiting. The queue replays on launch, when the connection returns, on
foreground and every 30 seconds while anything waits, then says "N changes
synced". A change the server refuses is dropped and reported once. Several
switches or edits of one source are merged; nothing is reordered across
sources. A queued note has a local id until it syncs; a lost answer to its
insert is found by its id, so it is never saved twice. Another account
never replays this account's queue; signing out drops it, as it drops the
saved copies. AI, deletes and everything else stay online only.

The root layout also starts quick actions and names the share viewer's
route after the sign-in group; the Library's panes scroll fields clear of
the keyboard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Share links and universal links, the first run, the trial and session
notices, the accessibility rules, the offline queue, Word export and the
development-build widget, quick actions and Spotlight.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A no-op until the development build has expo-core-spotlight. Comments no
longer point at the removed native-config file.

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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
supabase-js refreshes the stored session as soon as its module loads and
removes it when the server refuses the refresh token, which can happen
before the auth provider mounts, so the provider missed it (the web run
showed no notice). The session storage now tells a removal nobody asked
for from the student's own sign-out (session-expiry.ts), counting a
removal only when a session was stored: supabase-js also removes the key
on every signed-out launch. Tests cover a refused refresh, a signed-out
launch, a sign-out, other keys, and a new sign-in clearing the notice.

The web preview's menus cap their text at 1.6x, as iOS menus stop
growing; at the largest size the Library's sort menu broke words and hid
its lower items.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…it leaves the view

From the Codex review of 9e2ceb4:
- When a note's insert reached the server but its answer did not, changes
  folded into the note meanwhile were dropped: the replay found the row and
  called it synced. It now saves the fields that differ first (tested).
- An edit queued from an open source wrote the draft into the view's own
  state, so a refused replay left the unsaved text on screen with no marker.
  A queued edit now shows only through the queue, and the view reads the row
  again whenever sources change, so it ends on what the server holds.

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

From the Codex review: the brainstorm transform read fields before the
schema checked them, so public share data such as a title whose toString
is not a function threw inside the viewer's promise and left it loading.
readBrainstorm now treats that as not a brainstorm (tested with that
payload), and the viewer catches any reader error and any failed read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the Codex review: a cold link straight to a tool (a share link, the
widget, a quick action, /app/generate) had no tabs beneath it, so the iPad
sidebar found no stack to close and piled tools up, and Back had nowhere to
go. The root layout anchors on the tabs and the tabs on the Debate tab;
the sidebar also starts the stack again from the tabs if they are ever
missing. The sidebar screenshots check that case: a cold /generate, then
Ask and Attack from the sidebar, then Back reaches the Debate tab.

The screenshot script gains KEEP_GOING=1, which records a failed group and
runs the rest.

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>
…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>
…keeps its open edit

From the Codex review of 27ab8f5:
- A queued change is written into the saved copies so it shows offline.
  When the server refused it on replay only open screens re-read, so after
  a relaunch an offline Library could show the refused change again. The
  replay now drops the copies the change touched (tested).
- The iPad Library keyed its source pane by the note's current id, which
  changes from local:<uuid> to the row's id when the note syncs, so the
  pane remounted and lost an edit in progress. It is keyed by the id that
  was opened; the view follows the new id itself.

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>
Programcode and others added 21 commits October 28, 2026 19:44
…ource closes its pane

From the Codex review of e946150:
- Dropping the Library's saved list on a refused replay also hid notes
  still waiting to sync if the connection went again, because waiting
  notes show over a list that could be read. A refusal is the server's
  answer, so the replay now reads the debate's sources and the affected
  source back into the copies; a refused new note leaves the list copy
  (tested).
- On iPad, deleting a synced note from the list's row action left its pane
  open on "source missing", because the synced note still counted as a
  row. The list now closes the pane when it deletes the open source, as
  the pane's own Delete does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A screen opened straight from a URL now has the Debate tab mounted beneath
it, unfocused, and that tab loads only once shown, so its hidden spinners
kept every such shot waiting. The expired-session page's refused refresh
is expected and no longer reported as a problem.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Ollie moves as the website's owl does (src/index.css, "The owl's
  motion"), ported to Reanimated on the UI thread: lids blink over each
  pose's eyes on the owl's own clock, a slow breath from the feet, the
  pose's wave, cheer or hop when it arrives or changes pose, and a pop on
  the website's spring. Avatars blink only where asked, the mark from 28
  points, and the loader bobs as the website's owl-bob.
- The tactile buttons spring down onto their ledge as a finger lands and
  bounce back on release; main and primary buttons tap.
- SuccessCheck: the mint badge pops on the overshoot curve and the tick
  draws in (.success-pop, check-draw), in the toast, the sign-in code
  notice, document notices, Ask's Saved and the source's saved line.
- theme/motion.ts holds the website's duration, easing and spring tokens;
  Skeleton (the website's shimmer) and PressScale come next.
- Everything stays still under Reduce Motion.

The screenshot script prefers reduced motion so shots show screens at
rest; MOTION=1 plays the motion and RECORD_DIR records videos. It also
fixes a NodeList spread the scripts' type check refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The iPad sidebar's chevron turns on the pop spring, its rows slide in and
out and the sections below glide into place (Reanimated layout
animations, replacing LayoutAnimation), and rows press in. A live draft's
sections pop in as each is started. The owl avatars that stand for work
in progress (Ask thinking, a live draft, Regenerate) blink, as the
website's present avatars do.

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.
…ote is listed once

From the Codex review of 45c8d9f:
- A refused change was only cleared from the saved copies by reading the
  server again, so if that read failed (the connection went again) an
  offline relaunch showed the refused value. The queue now remembers what
  the copies showed before each queued change, field by field and kept
  with the queue, and puts it back on refusal before reading the server;
  a field a newer queued write still changes keeps that newer value.
- A synced note kept its temporary row in the saved list next to its real
  row, so a failed list read afterwards showed it twice. The saved list
  now swaps the temporary row for the row.

Both have tests that fail without the change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Loading lists and documents show skeletons in their own shape (the
  website's shimmer) instead of spinners: the Debate tab (page, counts,
  recent work and sources), Library, History, Prepare, Ask's chat list,
  saved maps, the context Review, the switcher, the You plan card and
  document loading. Each is the progressbar for VoiceOver. Spinners stay
  only inside buttons and inline busy controls.
- Rows and tiles fade up into place 40 ms apart on first appearance
  (capped at eight steps), and rows slide into the gap a deleted one
  leaves. Cards, tiles and rows press in on a spring; Link previews,
  menus and swipe actions are unchanged. Choosing a list item taps.
- The Spar card's owl blinks.
- Reanimated layout animations follow the system's Reduce Motion.

Also from the Codex review of 45c8d9f: deleting a source closes its
iPad pane only if that source is still the one open when the delete
lands, so a source opened meanwhile keeps its pane and its draft.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Finishing a Sparring round, a Crossfire round or a freshly generated case
opens a short celebration over the result: Ollie cheers, confetti bursts
in the brand colours (one Reanimated progress value, every piece's flight
a worklet), the mint check pops, and the stats that already exist there
count up (Sparring: your score, speeches, time used; Crossfire:
concessions secured, time used; a case: arguments and sources), with the
existing labels. It leaves after 2.4 s, on a tap or on Continue
(debate.continue), shows once per finish and never on reopening, and is
one button for VoiceOver that announces its content once and stays until
dismissed when a screen reader is on.

The Sparring scorecard's category bars fill from empty on a spring and
its cards pop in staggered; the Crossfire debrief's sections pop in the
same way, after the celebration leaves. Under Reduce Motion: no confetti,
cheer or count-up, only a short fade.

The screenshot script shoots each celebration, taps it away and carries
on with the existing shots.

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

The celebration's backdrop was 95% opaque, so the case behind it ghosted
through its title and stats; it is now solid, as Duolingo's are. The
screenshot script expects the Attack prefill to start with the v2 case's
OPENING, and the case-edits group taps a new case's celebration away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the Codex review of 958f143:
- Reanimated's useReducedMotion reads the setting once at launch, so
  turning Reduce Motion on in Settings left the owls blinking and
  celebrations counting up. Every animation now uses one useReduceMotion
  (theme/motion.ts) that starts from that reading and follows the setting.
- The values a refused change goes back to were noted while the copies
  updated, so a refusal arriving first restored nothing; the queue now
  waits for pending copy updates before settling (tested).
- A saved write did not move the restore point when a newer write to the
  same field was still queued, so refusing that one went back past the
  saved value; it now goes back to what the server holds (tested).
- The saved-list swap for a synced note (from round 4) superseded the
  list refresh addSource starts, leaving no list to read offline. It is
  gone; the duplicate it addressed came from the Library's own merge,
  which now replaces the note's temporary row with its row.

The queue publishes its new state before the copies are put back, so a
caller that hears a refusal already sees the write gone.

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

The iPad sidebar drew every section open until the stored choice was
read, so a closed section flashed open and shut on launch (the motion
recording caught it). Rows now wait for that read, and rows shown when the
sidebar first appears are simply there; a tap still slides them in.

The web preview's Library menus are anchored under their buttons rather
than a fixed 50 points down, so at the largest text size they no longer
cover the button that closes them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fake answered a PATCH without applying it, so the list read that
follows every write brought the old Active value back and the switch check
failed or passed by timing. It now keeps the change, as the server does.

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

From the Codex review of db35dab:
- Reanimated's layout animations follow the setting it read at launch, so
  turning Reduce Motion on later still slid list rows, sidebar rows, live
  draft sections and toasts in. Each now takes the live setting
  (motionSetting with useReduceMotion).
- A note whose insert landed but whose answer was lost synced through the
  duplicate path, which skips addSource's refresh, so the saved copies
  kept it under its temporary id and it could not open offline after a
  restart. That path now gives the copies the row (tested; the test fails
  without the change).
- Turning Reduce Motion on during a celebration's count-up froze the
  number short of its value; it now shows the final number.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #265: the Practice tab was the only empty state without Ollie, and the judging ring showed a blue check where success is always mint with a check mark.
…e; older R2 cases regenerate

From the Codex review of 0c22ab7:
- Clear all left notes and changes still queued, so they came back and
  were inserted after the clear. It now lets a running replay finish and
  drops the debate's queued writes before clearing (tested).
- When duplicate recovery also saved folded edits, updateSource dropped
  the saved Library list and nothing read it back unless the Library was
  open, so it could not load offline. Recovery now reads the list back
  from the server (tested).
- Regenerating Arguments on an older R2 or Conclusor case took only the
  new run's empty contentions and dropped its reinforce or whyWeWin, so
  the section went empty. The shared merge keeps them (tested in core;
  the website uses the same merge).

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

From the Codex review of deb28ba:
- Clear all dropped queued notes and changes before the server cleared,
  so a clear that failed lost an unsynced note for good. The queue now
  holds its replay while the clear runs and drops the debate's writes
  only once it succeeds; a failed clear keeps them and the queue resumes
  (tested).
- The v2 case document keyed arguments by the model's id, so two
  arguments with the same id opened and closed together; they are keyed
  by position again.

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

From the Codex review of 48dd067:
- A change made offline was reported as queued before the queue holding
  it was saved, so if storage failed the note was gone after a restart
  while its draft had closed. It is now confirmed only once saved (with
  one retry); if it cannot be kept it leaves the queue, the copies go
  back, and the caller hears it, so the draft stays open (tested).
- If the stored queue could not be read at launch, the next save wrote
  over it and lost earlier unsynced work. Nothing is written over it until
  a read succeeds; each new write reads it again first, and stored writes
  go ahead of newer ones once read (tested).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fore the queue is read

From the Codex review of df0db7d, which found that taking a change back
out when its save failed could delete earlier work, leave it stored, or
reject a change that then synced:
- A change the phone cannot store now stays in the queue and syncs while
  the app is open. The student is told once ("Not saved on this phone
  yet", keep the app open) and the save is tried again shortly. Nothing
  is taken back (tested).
- Until the stored queue has been read, every write is refused, online
  ones too, so none goes ahead of older unsynced work and Clear all
  cannot leave stored notes behind (tested); ownership is checked again
  after the read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…early failed session read keeps it

From the Codex review of fd3cf3d:
- A change sent while the queue could not be stored could not be retired
  on the phone, so after a relaunch it was sent again over newer work.
  While storage fails, the queue sends nothing (a replay stops after a
  save that failed) and refuses new writes with a clear error; it carries
  on once a save works (tested).
- The "Not saved on this phone yet" warning came before the caller's own
  "saved on this phone" message, which replaced it. It now comes after,
  and again for each change made while storage keeps failing.
- No session before auth had settled (an early read that failed) counted
  as signed out, which removes stored queues. The queue's owner now
  changes to nobody only once auth has settled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A queue save whose account changed before it ran wrote nothing but still
resolved as a success: its caller heard the change was kept on the phone,
and the storage-failure flag was cleared. It now rejects those callers
with the account-changed error (the switch removes that account's queue,
as before) and leaves the flag alone (tested; the test fails without it).

A concurrency test pins that a change whose retirement save fails is sent
once, however often replay is asked: the sent change has left the queue
before that save, and replay is single-flight, so clearing inFlight there
cannot cause a second send.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings native-ios/parity-r4-polish (#265) into native-ios/parity-r3-practice
(#262) so both rounds ship and build together.

Conflicts kept both sides: r3's voice rounds, dictation, timer runner,
Practice free tools and @expo/ui 57.0.23, with r4's motion, keyboard Done
bar, iPad ⌘↩, VoiceOver announcements, celebrations, sidebar, share links,
offline queue and first run. In a voice round the opponent speaks its turn
aloud; otherwise VoiceOver reads it.

The iPad sidebar now ends Practice with the speech time calculator and the
motion generator, and keeps Crossfire and Timer highlighted on a saved round
and the custom format sheet. Spotlight domains use com.pistack.debateowl.

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

coderabbitai Bot commented Oct 29, 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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

… bar

Round 3's custom words-per-minute field was a plain TextInput, so after the
merge it missed round 4's Done bar. Its number pad has no Return key, so the
keyboard could not be put away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.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