Skip to content

iOS parity round 4: case v2, iPad sidebar, share links, offline queue, first run, accessibility and Duolingo-style motion - #265

Merged
Programcode merged 39 commits into
feat/native-iosfrom
native-ios/parity-r4-polish
Oct 29, 2026
Merged

Programcode merged 39 commits into
feat/native-iosfrom
native-ios/parity-r4-polish

Conversation

@Programcode

@Programcode Programcode commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

Round 4 of iOS parity (~/.t3/scratch/ios-parity/PARITY.md, "Round 4" and section 2), plus the grouped iPad sidebar Adam asked for. Everything runs in Expo Go except the parts listed under "Needs a development build", which are written and gated so Expo Go is unaffected.

What changed

Case format v2 (#244). origin/main is merged in with a normal merge. Main's #244 changed website files that round 1 had moved into packages/core, so the merge keeps the website's re-exports and ports main's changes into core: the live draft sections, the transform, the regenerate merge, the R2/Conclusor argument check (now hasExpectedArguments, shared) and the flowchart built from readCase. The app renders cases with readCase as the website's GenerateResults does: opening (hook, definition, "What we will show"), arguments with explanation, evidence and why it matters, R2's backed-up arguments, Conclusor's "Why we win", "If they say… / We answer", closing line and sources. Older cases show their sections under the website's plain names. PDF, copied text, Discuss and the Regenerate list follow it.

iPad sidebar (Adam's addition). A full-screen iPad window shows the app's own sidebar beside the root stack, so it stays while a tool is open:

  • The Current Debate switcher and New debate sit on top.
  • Below them come collapsible sections: 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.
  • Closed sections are remembered on the device. The current screen is highlighted, including a tool's saved documents, and a closed section holding it shows a dot.
  • VoiceOver reads the section headers as headings with their expanded state. Rows grow with Dynamic Type, and the sidebar widens with the text size up to a cap.
  • The native tab bar hides there. Split View, Slide Over, Stage Manager windows and widths under 700 keep the tab bar.
  • Screens lay out for the room beside the sidebar through ReservedWidth.
  • Round 3's small Practice tools aren't on this branch yet. They go at the end of Practice in sidebar-items.ts.

Share links. Saved cases, attacks, brainstorms and factoids have Share link in their menu:

  • It calls the existing share Edge Function and opens the share sheet. A link made this session for the same unchanged result is reused, because every call makes a new link and counts toward the 50-a-day limit.
  • share/[code] is a read-only viewer that works signed in or out, through the public get_shared_item RPC.
  • Staging links point to production: with no browser Origin the function falls back to PUBLIC_WEB_APP_ORIGIN, which defaults to https://debateowl.com (supabase/functions/_shared/cors.ts). That holds unless staging sets PUBLIC_WEB_APP_ORIGIN, a config change not made here.

Universal links, app side. +native-intent.tsx maps https://debateowl.com/share/<code> and /app/<page> (and www) to app routes; any other web address opens the Debate tab. The parser is tested against other hosts, ports, user info, encoded segments and javascript: URLs. Development builds get the Associated Domains entitlement. The website's apple-app-site-association file is a follow-up for Adam (below).

Accessibility.

  • Every text field has a Done bar above the keyboard.
  • Tabs, tool forms, documents, the Library panes and the Flowchart scroll fields clear of the keyboard.
  • VoiceOver hears a finished Ask answer, Crossfire reply, Sparring speech or tool result once, never token by token.
  • More section titles are headings, and swipe rows already had their custom actions.
  • Text grows with Dynamic Type everywhere. Only fixed chrome is capped: floating action-bar buttons and segmented controls at 1.6, the sidebar at 1.35.
  • iPad shortcuts: ⌘N new debate, ⌘K switcher, ⌘↩ send in Ask, Crossfire and Sparring. They work in the web preview; on iOS they need a UIKeyCommand module in the development build. Space for the timer belongs to round 3's timer work.

Home Screen widget, quick actions, Spotlight (development build).

  • The Current Debate widget comes in small and medium: the motion, sources, results and rounds, plus a Spar link. The debate context and the Debate tab publish to it.
  • Quick actions (New debate, Start timer, Ask your coach) and Spotlight (the Current Debate's cases) are written behind nativeCapabilities(). They switch on only once expo-quick-actions or expo-core-spotlight is installed. I didn't install either: neither has been tried on SDK 57 on a device.

Offline queue.

  • New notes, source edits and Active switches go through one queue kept on the phone under the user id. Without a connection the change shows at once with "Waiting to sync".
  • It replays in order on launch, reconnect, foreground and every 30 seconds while anything waits, then says "N changes synced".
  • Changes are merged per source. A change the server refuses is dropped and reported once. Another account never replays this account's queue.

First run and notices.

  • Ollie's welcome animates line by line as on the website, followed by the website's four-step tour as native pages. The welcome shows once per phone and the tour once per account, under the website's keys.
  • The Debate tab shows the trial banner ("Free trial: 2 days left."); the day count moved to packages/core/src/trial.ts.
  • The sign-in screen says "Your session expired. Sign in again." when the server ended the session (refused refresh, on launch or later), never after a sign-out the student asked for.

Word export. The website's DOCX builders moved unchanged to packages/core/src/export/docx.ts. Every part of the generated files (534 files across English and Hungarian, every tool) is identical before and after. The app packs them with Packer.toBase64String and shares the .docx. docx compiles with hermesc and runs with no Node or DOM globals; it hasn't run on a device.

Motion, the Duolingo pass (animations only; no new features, every label kept).

  • 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.
    • The logo mark and the avatars that stand for work in progress blink; the loader bobs. The web has no Lottie or Rive files, so this ports its CSS keyframes.
  • The 3D buttons spring down onto their darker ledge and bounce back, with a light tap on main actions.
  • The mint check pops on the overshoot curve while its tick draws in (toasts, sign-in, saved notices).
  • Lists and documents load as skeletons with the website's shimmer instead of spinners; rows fade up 40 ms apart and slide into the gap a deleted row leaves; cards, tiles and rows press in.
  • The iPad sidebar's chevron springs and its rows slide; live draft sections pop in.
  • Finishing a Sparring round, a Crossfire round or a freshly generated case opens a celebration: Ollie cheers, confetti, the mint check, and the existing stats count up. It shows once per finish and is one dismissable button for VoiceOver.
  • Sparring's score bars fill with a spring; scorecard and debrief cards pop in staggered.
  • Reduce Motion follows the system setting live, and everything is still under it. The screenshot script runs with reduced motion so shots show screens at rest; MOTION=1 plays motion and RECORD_DIR records video.
  • No streak feather: no streak exists on the website or in the app, and adding one would be a new feature.
  • Native stack pushes keep iOS's own timing; the motion lives in buttons, lists, cards and the owl.

Screenshot script.

  • New groups: sidebar, first-run and share. The share group makes one real link on staging and opens it in the viewer.
  • FONT_SCALE draws a Dynamic Type size in the web preview.
  • FUNCTIONS_ORIGIN lets the script use any port while relaying Edge Function calls with an allowed Origin, so parallel worktrees don't collide on 18080.

Verification

  • npm run verify passes locally on 346d6a29: docs check, lint (eslint and knip), typecheck (website, agentic, core, mobile), vitest 870 tests, collaboration 30, Edge Function tests 553, build, service-worker test, and Playwright smoke 47 passed. GitHub Actions is billing-blocked, so this is a local pass, not CI.
  • Twelve Codex review rounds (gpt-6.1-sol, xhigh) on pinned snapshots, rounds 1 to 11 on earlier heads. Every finding was fixed with a test where the logic allowed, and each new test fails without its fix:
    • round 1: 4 findings;
    • rounds 2 and 3: 2 each;
    • rounds 4 to 7: 3, 4, 3 and 3;
    • rounds 8 and 9: 2 each;
    • rounds 10 and 11: 5 and 3.
      Round 12 (on 4638ab5d) hung and was cancelled.
  • After round 12, two suspected queue issues were checked by hand in 346d6a29:
    • A save whose account changed before it ran was reported as a success. It told its caller the change was kept and cleared the storage-failure flag. Fixed, with a test that fails without the fix.
    • The suspected duplicate send after a failed retirement save is not a bug: the sent change has already left the queue by then, and replay runs one at a time. A concurrency test pins this.
  • npx expo config --json without DEBATELAB_BUILD is byte-identical to before, so the Expo Go config is unchanged. The development build gains Associated Domains, and with DEBATELAB_APP_GROUP the CurrentDebate widget.
  • Real staging: the share group makes a real share link and opens it in the in-app viewer.
  • The web export, driven with Playwright through the screenshot script, covers:
    • the sidebar checks, including Split View at 600 points and a cold link followed by Back;
    • the expired-session notice, with a refused refresh;
    • the trial banner and the first run;
    • celebrations on Sparring, Crossfire and a new case;
    • the v2 case document.
  • Not verified (needs a development build or a device):
    • native VoiceOver announcements;
    • the iOS Done bar;
    • Dynamic Type on device;
    • the share sheet;
    • universal links;
    • the widget, quick actions and Spotlight;
    • the hardware-keyboard shortcuts on iOS;
    • 60 fps on device;
    • the Word file opening in Pages or Word;
    • opening a queued local: note.

Screenshots

In ~/.t3/scratch/ios-parity/r4-shots/ (on the VPS):

  • 663 shots: iPhone light and dark (English), iPhone light (Hungarian), iPad portrait light and dark (English), iPad light (Hungarian), 11-inch iPad landscape dark, and a 600-point Split View window.
  • ax/: 115 shots at the largest accessibility text size (FONT_SCALE 3.1), iPhone and iPad.
  • motion/: 26 screen recordings (WebM) of the animations with motion on, including ipad-sidebar-sections.webm.

New states worth opening first:

  • ipad-light-en-sidebar-expanded.png and ipad-air-landscape-dark-en-sidebar-collapsed.png: the grouped iPad sidebar.
  • iphone-light-en-case.png: a case in the v2 format.
  • iphone-light-en-case-celebration.png, iphone-light-en-sparring-celebration.png and iphone-light-en-crossfire-celebration.png: the celebrations.
  • iphone-light-en-welcome.png, iphone-light-en-tour-debate.png and iphone-light-en-tour-ready.png: Ollie's welcome and the tour.
  • iphone-light-en-trial-banner.png and iphone-light-en-session-expired.png: the two notices.
  • iphone-light-en-shared-case.png: a share link opened in the app.
  • iphone-light-hu-case.png: a case in Hungarian.
  • ax/iphone-light-en-debate.png and ax/iphone-light-en-library.png: the largest text size.

Needs Adam

  • Apple Developer account. Provisioning for a development build with:
    • Associated Domains (applinks:debateowl.com, applinks:www.debateowl.com);
    • an App Group (DEBATELAB_APP_GROUP) for the widget, which round 3's Live Activity also needs;
    • later, Push.
  • Bundle id decision. It's still com.adam.debatelab, and the AASA appIDs below depend on it.
  • The website's AASA file and its deploy. Serve https://debateowl.com/.well-known/apple-app-site-association (and 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.adam.debatelab"],
            "components": [
              { "/": "/share/*", "comment": "Shared results" },
              { "/": "/app/*", "comment": "App pages" }
            ]
          }
        ]
      }
    }
  • Optional staging config. Set PUBLIC_WEB_APP_ORIGIN on staging so staging share links open the staging site.
  • Packages to decide on for the development build.
    • expo-quick-actions (its compatibility table stops at SDK 54).
    • expo-core-spotlight@^2, which supports SDK 56–57 but whose plugin patches AppDelegate.swift.
    • A UIKeyCommand module for the hardware-keyboard shortcuts.
  • EAS build within the budget in apps/mobile/AGENTS.md.
  • Device test of everything marked "needs dev build" below, plus:
    • VoiceOver announcements;
    • the Done bar;
    • Dynamic Type at AX5;
    • the share sheet;
    • opening a queued local: note;
    • the Word file in Pages or Word.

🤖 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>
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>
…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>
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>
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>
…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>
…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>
…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>
Copilot AI balanced review requested due to automatic review settings October 28, 2026 21:58
@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.

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

Programcode and others added 8 commits October 28, 2026 22:12
…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>
@Programcode
Programcode merged commit 3ad823c into feat/native-ios Oct 29, 2026
3 of 14 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