Skip to content

iOS parity round 1: prepare tools match the web - #254

Merged
Programcode merged 14 commits into
feat/native-iosfrom
native-ios/parity-r1-prepare
Oct 28, 2026
Merged

Programcode merged 14 commits into
feat/native-iosfrom
native-ios/parity-r1-prepare

Conversation

@Programcode

@Programcode Programcode commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

Round 1 of the iOS/iPadOS parity plan (~/.t3/scratch/ios-parity/PARITY.md, section 3). The app's prepare tools now work the way the website's do, in Expo Go, with no backend change. Where the website already had the logic, it moved to packages/core, and both clients use it from there.

Items

1. Live streaming for Generate, Attack, Brainstorm and Factoids: done.

  • The four tools send stream: true with a resultKey. The screen shows the phase (researching, writing, checking), the elapsed time from 5 s, and then the result's sections filling in with a caret.
  • When a stream drops before its answer, the app reads the answer back from ai_run_results, as the website does.
  • The stream reader, the recovery loop and the draft sections moved from src/ to packages/core/src/ai/{live,run-recovery,live-sections}.ts. The website's modules are now thin adapters with the same exports, and its tests run unchanged.
  • Proof comes from two places. The "-live" shots run the web export against a fake stream inside the page. ai-request.test.ts covers a streamed answer, a refusal inside the stream, a dropped stream recovered from ai_run_results, and a dropped stream with no stored answer.
  • Real runs on staging, from the app's web export in one-off mode (nothing saved), all on anthropic/claude-sonnet-5.5. Adam approved resetting the seeded account's weekly allowance for them.
    • Generate streamed (text/event-stream). The screen showed researching, writing, writing live and checking, the elapsed time, and the first section after 12 s; the case finished at 44 s.
    • Attack, Brainstorm and Factoids streamed the same way, with the first section after 8 to 14 s.
    • Recovery: a real Generate whose stream I cut before its result event. The app read ai_run_results and opened the finished case at 43 s.
    • Before the reset, a real Generate met weekly_budget_reached, and the app opened the weekly-allowance sheet with only OK, as the website does.
  • Besides Claude, staging's Generate calls typesafe/jev-1.13 through OpenRouter to rank sources and check which ones the case used.

2. Generate: Regenerate one part, Undo, Save with a title: done, with one deliberate difference.

  • "Regenerate a section" in the case menu lists the website's section cards. A new generation replaces the chosen part with the website's merge (mergeRegeneratedSection, now in core and used by GeneratePage).
  • Undo goes back to the version before. A real Regenerate on staging replaced the arguments in 43 s with the website's notice.
  • Save opens a sheet with the website's title prompt. The saved confirmation is mint with a check mark.
  • Difference: after Undo the website leaves the saved row on the newer version. The app writes the restored version back, so History shows what is on screen.

3. Open in Crossfire and Build flowchart from case: done.

  • The case and attack menus have "Open in Crossfire". It fills the Crossfire form with the case, or with the attacked case and its cross-examination questions, and shows the website's import notice.
  • "Build flowchart from case" opens an unsaved map in the case's debate. flowchartFromCase moved to core, and the website uses it from there.

4. Debate context control: done, with two gaps.

  • Generate, Attack, Brainstorm, Factoids, Crossfire, Sparring and Ask show the website's line, such as "Using 3 sources and 2 saved results".
  • Review opens a sheet that turns sources and saved results off for the next request (for the whole round in Crossfire). It also pins up to three results in debate_context_pins.
  • The line, the sheet and the request all read one plan from the website's planDebateContext, which moved to packages/core/src/debate/context-plan.ts.
  • Gaps: the sheet does not show the website's "Your last request here" summary, and Ask hides the line while an answer is being written. Sparring's line says it uses only the motion, which is the website's policy for live rounds.

5. Daily-use line, round cost and limit sheets: done, with the purchase step changed.

  • Every tool header says "N of M AI uses left today" and reads it again after each request. Ask shows the line in its empty state only.
  • Crossfire and Sparring show the round's cost above Start and check it before the clock starts.
  • Limit and model refusals open the website's dialogs as a sheet, in the website's words.
  • Change: the app cannot sell Pro until in-app purchase arrives (round 5), so the upgrade button is "See your plan", which opens You. The payment-failed text still says "Settings" because it is the website's string. "Use low reasoning instead" switches the tool to low reasoning instead of only closing the dialog.

6. Ask: Stop, General/Clarify, Save to Library, text attachments: done.

  • Stop keeps the answer so far and marks it as stopped. On staging, a real answer streamed within 3 s, and Stop kept its first 363 characters.
  • Clarify, and the options ai-stream refuses, stream live through the ai function, as on the website.
  • Save adds the answer to the Current Debate's Ask folder.
  • Up to five text or code files attach through expo-document-picker, which Expo Go includes (npx expo config is unchanged). The phone reads them and sends them in the website's format, now shared from core.
  • Difference: the website's validator errors are English only, so the app explains a rejected file with new mobile.ask.* strings in en, hu and es.

Not in this round: case structure v2 (waits for #244), Library folders and import, debate edit or delete, haptics and context menus (round 2).

Also changed

  • @formatjs/intl-listformat, because Hermes has no Intl.ListFormat and the context line joins its parts with it.
  • The screenshot script gains the streamed "-live" shots, the case-edits, context and limits groups, the new Ask shots, and quota answers in its fakes. It also fixes two stale fakes: the sentence-case Brainstorm and Factoids buttons, and a Crossfire debrief that no longer fit its schema.
  • No migrations, Edge Function deploys or config changes. Nothing in this round needs one.

Review fixes (Cody)

  • Website: a live request that outlived its deadline became an unexpected error (and a Sentry report), because the shared reader rejected with a plain Error named AbortError. The reader now throws a DOMException where one exists, and both clients tell an abort by its name. A new test covers the website's timeout on a live stream that goes quiet.
  • "Use low reasoning instead" also moves the tool off a Pro-only model (including Auto Pro), so the retry is not refused again.
  • The debate context reads only titles, sides and dates on each form, and the text only of the items a request sends, kept for the session. A list read that ends after a newer one is dropped, and one that started before a pin change keeps the newer pins.
  • Crossfire and Sparring show Start as busy while the round's cost is checked, and ignore a second tap.
  • The case document confirms a save outside its state update.
  • A regenerated case keeps its new sources even if the student leaves, and a failure there no longer reads as a failed regeneration.
  • Opening a case or an attack in Crossfire while a round runs now says why it was not brought in (new mobile.crossfire.prefillHeld in en, hu and es).
  • A round's cost check no longer reloads the usage line.
  • Re-review: the text the debate context kept for the session never expired, so after a Regenerate or an Undo the plan judged the old case, and a chat kept its old messages. Kept text now carries the version the list read reports for its row: a case's output.metadata.generatedAt (Regenerate and Undo change it), a chat's message count, a Sparring round's updated_at. Attacks, brainstorms, factoids and Crossfire rounds are never edited. Only sparring_sessions has an updated_at, and adding one elsewhere would need a migration, so source text is read again after each list read instead. updateResult and saveChat also forget what they wrote. A row deleted between the list read and the text read is left out of the plan instead of failing the request. context-plan.test.ts covers a regenerated case, a chat with a new message, an unchanged row that is not read again, and a deleted row.
  • Not done: moving the trial length into packages/core. src/config/site.json is the website's one source for pricing facts (its i18n, prerender and pages read it), and the app reads it the same way.

Verification

I ran npm run verify locally at 112ad07, with both rounds of review fixes. GitHub Actions is billing-blocked, so this is a local pass, not CI.

  • docs:check, lint (eslint, knip, repo rules) and typecheck (website, agentic, core, mobile) pass.
  • Unit tests: 799 of 799 pass in 123 files. Collaboration: 30 pass. Edge: 543 pass, 0 fail.
  • The build, the service-worker offline check and the e2e smoke run (47 tests) pass.

Screenshots are in ~/.t3/scratch/ios-parity/r1-shots/: 761 shots for iPhone, iPad Air portrait and landscape, light and dark, English and Hungarian, plus iPhone light Spanish. Every AI answer in them is a fixed fake, and the runs save nothing. Contact sheets per screen are in r1-shots/contact/. After the review fixes I retook the limit, Crossfire and Sparring screens on iPhone in English and Hungarian (r1-shots/review-fixes/); they are unchanged apart from the fixes. The 40 shots in clipped.json all show text that was already shortened by design before this round: Ask's one-line motion under its title and the motion on Sparring's your-turn screen.

🤖 Generated with Claude Code

Programcode and others added 2 commits October 28, 2026 11:47
… core

The website's live generation reader, its ai_run_results recovery and the
live draft sections move to packages/core so the iOS app streams tools the
same way. The website keeps its modules as thin adapters with the same
exports, so its tests run unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The four tools ask the ai function to stream, as the website does, and show
the answer as it is written: the phase line and elapsed time first, then the
result's sections filling in with a caret. A stream that drops before its
answer reads it back from ai_run_results under the request's own id. A
refusal inside the stream keeps its limit details for the limit sheets.

The screenshot script streams part of each answer from inside the page for
new "-live" shots, and matches the sentence-case Brainstorm and Factoids
buttons.

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

Programcode and others added 8 commits October 28, 2026 12:10
The case and attack documents gain the website's "Open in Crossfire", which
fills the next Crossfire form with the case, or with the attacked case and its
cross-examination questions, and says where it came from. A case can be built
into an unsaved Flowchart map in its own debate. flowchartFromCase moves to
packages/core so the website and the app build the same map.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each tool's header says how many AI uses are left today, read again after
every request. Crossfire and Sparring say what a round costs before it
starts and check it first, as the website does. A limit or model refusal
opens the website's dialog as a sheet with its words: a paid plan's daily cap
and the weekly allowance say when access returns, other limits lead to the
plan in You (purchases wait for in-app purchase), and a model refusal can
switch the tool to low reasoning.

The screenshot script shoots both sheets and the cost notes, answers the
quota check in its Crossfire and Sparring fakes, and sends a debrief that
fits the current schema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A case's menu can regenerate one part (the opening, the arguments, the case
structure, the predicted opposition or the counters) from a new generation,
as the website's section cards do, with the website's merge now shared
through packages/core. Undo goes back to the version before, and writes it
back to the saved row so History matches the screen. Save opens a sheet with
the website's title prompt, and a saved case is confirmed in mint with a
check mark.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ask gains the website's Stop, which keeps the answer so far marked as
stopped, and its General and Clarify modes. Answers the stream cannot serve
now stream live from the ai function, as on the website. Each answer can be
saved to the Current Debate's Ask folder in the Library. Up to five text or
code files can be attached with expo-document-picker (in Expo Go); they are
read on the phone and sent with the question in the website's format, now
shared through packages/core.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every tool form, Crossfire, Sparring and Ask now show the website's line:
"Using 3 sources and 2 saved results", with the results named. Review opens
a sheet that turns sources and saved results off for the next request only
(for the whole round in Crossfire), and pins up to three results for every
request in the debate, in debate_context_pins as on the website. The line,
the sheet and the request read one plan: the website's planner, moved to
packages/core.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gth from the site config

Hermes has no Intl.ListFormat, so the debate context line fell back to a
comma; the formatjs polyfill joins it as the website does in all three
languages. The limit sheet's trial length now comes from the website's site
config instead of a copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The case menu now starts with Regenerate, and a wide iPad's outline rail is
a menu too, so the script finds the action menu and its items by their labels.

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

Crossfire, opened from a case or an attack, asks what a round costs; the
results fake aborted that call, which the browser logged as an error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Programcode
Programcode marked this pull request as ready for review October 28, 2026 13:34
Copilot AI balanced review requested due to automatic review settings October 28, 2026 13:34

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 3 commits October 28, 2026 14:07
The shared live reader rejected with a plain Error named AbortError, which
callSupabaseAI did not recognise as its own deadline, so a stalled stream
on the website became an unexpected error reported to Sentry. The reader now
throws a DOMException where one exists, and both clients tell an abort by its
name, since a DOMException is not always an Error. A test covers the
website's timeout on a live stream that goes quiet.

The app's usage line no longer reloads after a round's cost check, which
spends nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- "Use low reasoning instead" also moves the tool off a Pro-only model, so
  the retry is not refused again.
- The debate context reads a list of titles and dates on each form, and the
  text only of what a request sends, kept for the session. A list read that
  ends after a newer one is dropped, and one that started before a pin
  change keeps the newer pins.
- Crossfire and Sparring show Start as busy while the round's cost is
  checked, and ignore a second tap.
- The case document confirms a save outside its state update.
- A regenerated case keeps its new sources even if the student leaves, and
  a failure there no longer reads as a failed regeneration.
- Opening a case or an attack in Crossfire while a round runs says why it
  was not brought in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The text the context plan keeps for the session never expired, so after a
Regenerate or an Undo the plan judged the old case, a chat kept its old
messages, and edits from the website went unseen. Kept text now carries the
version the list read reports for its row: a case's generatedAt, which
Regenerate and Undo change; a chat's message count; a Sparring round's
updated_at. Attacks, brainstorms, factoids and Crossfire rounds are never
edited. No source column says when it changed, so source text is read again
after each list read. updateResult and saveChat also forget what they wrote.

A row deleted between the list read and the text read is left out of the
plan instead of failing the request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Programcode
Programcode merged commit 2f7aae1 into feat/native-ios Oct 28, 2026
3 of 13 checks passed
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