Skip to content

Add the native Swift iOS app and Swift SDK - #1152

Draft
AnthonyRonning wants to merge 28 commits into
masterfrom
codex-ios-native-swift
Draft

AnthonyRonning wants to merge 28 commits into
masterfrom
codex-ios-native-swift

Conversation

@AnthonyRonning

@AnthonyRonning AnthonyRonning commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Opens @marksftw's codex-ios-native-swift branch as a PR so review and the parity catch-up have a home. Branch and all nine commits are Mark's; PR opened on his behalf at Anthony's request.

What is on the branch

  • sdk/swift: MapleSDK, a Swift-native Maple SDK with no third-party dependencies. Transport V2 (X25519 + HKDF-SHA256 + ChaCha20-Poly1305 over CryptoKit, record framing, single-resend recovery), Nitro attestation (COSE_Sign1/CBOR, P-384, pinned AWS root, PCR0 allowlists plus signed GitHub history), credential authority with Keychain persistence, the Research API surface the app uses (conversations, items, Responses streaming, catalog, transcription, speech, account ceremonies), and the native OAuth handoff (prepare, redeem, confirm).
  • apps/maple-ios: Swift 6 / SwiftUI app for iPhone and iPad, iOS 26 minimum. Packages MapleAppCore and MapleAppUI; checked-in Maple.xcodeproj; xcconfig profiles Local / PR / Release with scripts/configure-local.py and scripts/build.sh. Chat with streaming, queue, drafts stored under Complete Protection, images, dictation, read-aloud player with lock-screen controls, library with pinned chats and projects, settings and account ceremonies. Markdown via a vendored Textual runtime (MIT, four local patches) and bundled offline KaTeX in a sandboxed WKWebView for equations.
  • CI and tooling: .github/workflows/ios-native.yml, .github/workflows/sdk-swift.yml, scripts/ci/ios-native.sh, scripts/ci/sdk-swift*.sh, change-detection registration and the pre-commit hook.
  • Docs: apps/maple-ios/README.md, apps/maple-ios/AGENTS.md, apps/maple-ios/docs/rendering.md, sdk/README.md.

Spec, decisions and reviews live in code-docs (docs/specs/ios-native/, PR #64 there).

Status

Draft. Verified on an iOS 26.5 simulator against a local Maple Dev Env stack on 2026-10-11 (sign-in, streaming, title refresh, Markdown, tables, KaTeX); Mark has tested it on TestFlight.

Known before-merge items, deliberately handled last:

  • Identity and lanes: bundle identifier and callback scheme, per-lane Auth and app hosts, production profile and PCR policy, entitlements and associated domains, App Store Connect tracks.
  • Apple IAP at the level Research ships, once the lanes and tracks are unified.
  • CI: master removed the macos-26-xlarge runner label and scripts/ci/test_select_xcode.py rejects inline Xcode selectors; both workflows here still use them.
  • The branch is 122 commits behind master with conflicts in AGENTS.md, .agents/skills/validate-maple/SKILL.md, scripts/ci/change_detection.py (and its test) and flake.nix.

The parity audit against Research iOS, with what gets fixed soon, what blocks the merge and what we pass on, is the first comment below and is the source of truth for the catch-up.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

Add native account, chat, library and audio workflows, branded rendering and
retained full-screen navigation, with encrypted SDK transport and CI checks.
Apply device-feedback refinements to launch, menu gestures, the composer,
chat options and project management, dictation, and transcript following.
Persist project expansion locally and require explicit chat deletion
confirmation. Retain version 0.1.0 build 4 and add interaction and ownership
regression coverage.
Lazy history rows previously discarded completed rich-text projections when
leaving the viewport because retention was limited to menu occlusion. On
re-entry, raw Markdown appeared before asynchronous formatting rebuilt the
reply, changing its height and destabilizing the transcript.

Retain completed message-local projections while the exact account generation
and selected-chat storage owner remain current. Reuse suspend() to cancel work
and reject late publication without clearing completed formatting. Keep owner
replacement and cancellation behavior intact.

Add repeated suspension and production-transcript scrolling regression tests,
synthetic rich-history fixtures, and content-free DEBUG lifecycle diagnostics.
Document the lifecycle contract and increase the native build number to 5.

Validation:
- 437 package tests and strict Swift formatting checks passed.
- Six focused iPhone/iPad simulator UI executions passed, including history,
  pagination, streaming, and retained-menu checks.
- Negative control cleared 8 completed projections; the fix retained all 44
  observed iPhone and 32 iPad disappearances without clearing any.
- Optimized unsigned build 5 archive verified against the reviewed source.
- User confirmed build 5 fixes the reported issue on their physical device.
Wide streamed tables could shift the outer vertical scroll view sideways by
20 points even when viewport and content widths were unchanged.

Give the transcript a viewport-based width capped at 850 points with stable
20-point gutters, and set x to zero for programmatic vertical scrolling.
Add a content-local UIKit guard that observes the nearest ancestor scroll
view and restores its horizontal inset while preserving the current y.
Keep the guard scoped to the current account/chat and active scene, with
observation cleanup on suspension, removal, or rebinding. Nested table and
code scrollers retain their independent horizontal scrolling.

Add a staged streaming regression covering prose, wide tables, code,
completion, nested table swipes, and vertical dragging. Document the axis
contract and increase the native build number to 6 for hardware verification.

Validation:
- 437 package tests and strict Swift formatting checks passed.
- New streaming regression passed on iPhone 17 Pro and iPad Pro 13-inch,
  both iOS 26.5; related rich-history, pagination, reading-anchor, and
  user-bubble/tool-row checks also passed.
- Release cross-builds and unsigned simulator/device builds passed.
- Before/after evidence shows the prior 20-point shift and restored displayed
  gutters. Intermediate layout offsets remain diagnostics, not proof of
  displayed movement before synchronous correction.
- Optimized unsigned build 6 archive verified; physical-device testing pending.
Replace the custom compact Menu/Chat transition with a retained
UINavigationController so UIKit owns drag progress, flick completion, and
cancellation. Fence native bar publications and retained content by the
current account/controller lease and chat owner, and remove obsolete
custom gesture code and tests.

Stage the selected existing-chat runtime before closing the menu, then
load history without delaying navigation. Keep its empty transcript blank
instead of displaying the New Chat greeting. Make the menu control icon
only, add Chat Options top clearance, and advance the build number to 8.

Validation: strict formatting and 433 unit tests passed; release iOS
package cross-builds and unsigned app builds passed. Focused iPhone/iPad
checks cover native flick/cancellation, nested scrolling, picker navigation,
and blank existing-chat entry before a held history load completes.
@AnthonyRonning

AnthonyRonning commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor Author

Parity audit: Research iOS vs the native Swift app (source of truth for the catch-up)

Compared: Research iOS on Maple master 2f22d337 (3.4.4, 2026-10-12) against codex-ios-native-swift at 4232814f (build 8, 2026-10-12). Three read-only area audits (account/auth/billing 63 rows, chat/composer 197 rows, library/settings/lifecycle 86 rows) plus the technical contract layer (15 rows). About 260 of the 361 rows are already at parity or are Mark's accepted idiom under D6/D25/D26/D34–D40. The rest is sorted below. Row-level tables with file:line evidence for both apps exist; I can commit them under code-docs docs/specs/ios-native/reviews/ if wanted. Decision IDs (D1–D40) refer to the code-docs decisions record.

Rules applied (Anthony, 2026-10-12/13): no product features the Research iOS app does not have; follow Mark's Swift/UI patterns and extend existing components rather than invent; UI/UX/brand calls belong to Mark and Michael; identity, lanes and App Store Connect cleanup are the final pass and a merge blocker. Master changes since Mark's base 516eb894 that matter here: #1089 thinking levels, #1064 error codes, #1100 mobile copy, #1024/#1105 deep links, #1141 production Auth broker, #1146 trimmed PCR lists.

Progress 2026-10-14: pass 2 (#1155) is merged into codex-ios-native-swift at 71a6e506 with one change by Anthony: the usage component lives in Settings, not the menu footer. Everything struck below is on the branch. What is left in §1 is account surfaces (verification gate, guests, billing page, API & credits, delete-account warning), team, documents, and the entitlements file that belongs to the lane work.

Progress 2026-10-13: pass 1 (#1153) is merged into codex-ios-native-swift at 6baf139c. Struck-through items below are done there. Also landed in #1153 beyond this list, per the "no gates Research does not have" rule: the client-side paid gate on web search and the catalog/plan gate on read-aloud were removed (Research has neither; D11 is superseded on those two), and the compact navigation controller now reconciles after any chat presentation is dismissed (a pre-existing bug in the base where Menu and New chat stopped responding after the options sheet).

1. Fix soon (catch-up work, decided)

1a. Contract layer, first because everything else builds on it

  • Catalog reasoning + max_completion_tokens on models and aliases, ResponsesCreateRequest.reasoning, ChatSendOptions.reasoningEffort, LocalPreferences.thinkingLevel (sdk/swift/Sources/MapleSDK/API/ResearchModels.swift, ResponsesAPI.swift).
  • Classify limit errors by x-opensecret-error-code / error.code with the substring match as fallback; add model_not_available_on_plan and unsupported_value (ResearchAPI.swift:359-409).
  • PCR0 roots: PCRTrustRoots.swift embeds 20 stale values and neither current root; replace with current + rollback per environment and put the refresh on the release checklist.
  • The five SDK findings from the 2026-10-11 Swift SDK review (code-docs): cache-namespace root sent on anonymous requests, Keychain write failure aborting session establishment, fatal 256-event stream drop, corrupt persisted authority, refresh coalescing on cancel.
  • Billing status: decode stripe_customer_id, api_credit_balance, ios_iap_enabled, ios_us_external_link_enabled, conflict, pending_plan_change (BillingContracts.swift).
  • Zero starter code: remove the branch in AccountEntitlements.swift:186 and the fixtures in AccountEntitlementsTests.swift:111-120. Plan rules become Research's: product_name == "free" is free, pro/max/team substring is paid, nothing else is special.

1b. Chat correctness

  • response.failed / error currently shows nothing; show "Failed to generate response. Please try again." (ChatRuntime.settle).
  • Pending indicator (pulsing dots) before the first assistant item; "Chat Canceled" chip on incomplete items after Stop.
  • 413: "Message Too Large" alert with Research's copy. The current footnote says the conversation is too long, which is wrong.
  • Plan-aware usage_limit_reached copy; free_tier_token_limit_exceeded as its own case with "Start New Chat"; model_not_available_on_plan matched by code with the neutral "not available on your current plan" banner.
  • Polling like Research: open conversation at 2, 5, 10, 15, 20, 30 s then every 60 s including idle; recents list every 60 s while the menu is visible; title poll (0.5 s doubling to 10 s) while the title is "New Conversation". Title generation is a server background task spawned when the request is accepted (handlers.rs:4329, 6332), so one refresh at completion can miss it.
  • Legacy {"document":…} user messages render as raw JSON plus the whole extracted text; port the chip + full-text sheet.
  • Tool cards: titles carrying the query, URL or page count, status labels with spinner/check/X, output rendered as Markdown with the 150-character "Show more".
  • Thinking block: "Thinking" while streaming, "Thought for N seconds" after; split legacy <think> tags found in stored history (low priority).
  • Load older on backward scroll near the top instead of the button, keeping readingAnchor.
  • Remove the two extra capability gates Research does not have: web search gated on the model's toolUse, images on vision (AccountEntitlements.swift:223-242). The server handles non-vision models with image description.
  • Stop pre-blocking chat on can_chat, chats_remaining == 0, unknown plan or team seat mismatch (AccountEntitlements.swift:200-208). Research pre-blocks only free guests and lets the server 403 decide the rest.

1c. Composer

  • Thinking-level control inside Mark's options sheet (SettingsModelPicker): Default, Off unless mandatory, then supported_efforts; a stored level the model does not offer falls back to Default; recorded per queued message; sent as reasoning.effort.
  • Markdown list continuation on newline (-, *, +, [ ], 1./1); empty item removes the marker; fences skipped).
  • Image paste from the iOS paste menu straight into the composer (the + → Paste image sheet stays).
  • Model badges rendered; current model short name on the trigger; tapping a locked model explains why.
  • "Encrypted and private at every step" subline under the composer and the "AI can make mistakes. Check important info." footer.

1d. Library and navigation

  • Row indicators: active chat, pin glyph, running spinner, "completed, unread" dot (rows and collapsed folders), with accessibility values.
  • Wire the existing ProjectView as the project detail ("View project"), add the selection toolbar and instructions preview, open it after create.
  • Pull-to-refresh; "New project" disabled at 10 with "Maximum of 10 projects reached" (server-enforced, MAX_CONVERSATION_PROJECTS_PER_USER); empty copy for search and Recents; leave selection mode after a bulk action; pinned chats also listed inside their folder; persist the iPad sidebar state.
  • Menu footer usage tile (plan pill, % used, reset-label ladder, meter) plus a visible settings entry carrying the team attention badge. (landed in ios-native parity pass 2: chat, composer, and library #1155; Anthony moved the tile into Settings instead of the menu footer, and the attention badge sits on the existing Settings entry)
  • Preferences: font descriptions, voice groups with help copy, TTS default speed 1.2× as on Research (the chooser must be able to show it).

1e. Account, settings, trust

  • Entitlements file with com.apple.developer.applesignin and CODE_SIGN_ENTITLEMENTS; without it Sign in with Apple is absent at build time. The dev audience registration belongs to the lane work in §2.
  • Email verification gate: non-dismissable AccountFlow(.verification) for unverified non-guest users, resend with 30 s cooldown, Log out.
  • Guest sign-up: triple-checkbox warning with the iOS copy from Make Android consumption-only for subscriptions and credits #1100, one-time non-dismissable Account ID dialog, signUpGuest exposed through SessionServing/MapleClient; free guests get the "Subscription Required" dialog with Log out now and the purchase/restore actions when IAP lands (§2).
  • Email sign-up sends name "ANON" like Research; drop the Name field; keep the 8-character rule.
  • Billing settings page: plan card with raw product name and "Renews on" / "Expires on" by provider; Subscriptions list with state labels, notes and the conflict alert; "Manage card subscription" through the Stripe portal (POST on BillingService); "Contact support" for Bitcoin. Apple rows render from subscriptions[], but Apple purchase, restore and manage stay disabled until §2.
  • API & credits: settings entry, credits balance card, API keys list/create/one-time reveal/delete (three ResearchAPI methods).
  • Team: seat-mismatch card with counts and admin/member copy (and stop blocking the composer on it); Team settings with the setup form for an existing team subscription, member and admin dashboards, members, pending invites, Stripe portal; Invite members page. No team purchase, Research iOS has none either.
  • Delete account: billing-aware pre-warning with the per-provider copy (Apple, Stripe owner, team member) and Research's 503 copy.
  • Documents, Swift-native: pdf/txt/md with PDFKit text extraction and Vision OCR for scanned pages, Word, 10 MiB, the {"document":…} message prefix, chip with spinner and Send block, Pro gate, queue limit, context-limit tip. .docx yes; whether legacy .doc (CFB) is worth a port of word_extractor.rs is the one open scope question.
  • Auth-screen footer (copyright, Terms, Privacy via LinkBrowser); About tagline.

1f. Copy batches (one pass each)

  • Chat: placeholders, budget copy, image-description-unavailable, "No speech detected", "Stopped before sending", stopping and "finished before it could be stopped", recovered completion/stopped, generic rejection (passed on: Research shows the server message body, Mark's SDK deliberately never exposes it) and capacity copy, deletion pause; "Copied" feedback on code blocks and messages; attachment errors auto-clear.
  • Library: rename same-title guard, delete-chat title interpolation, bulk-delete count, instructions placeholder/help, delete-project copy with name and "Move chats out first".

2. Before merge (blockers, deliberately last)

  • Lane correctness: app(-dev).maple.ai / auth(-dev).maple.ai per lane, bundle identifiers and callback schemes (the Auth site admits only cloud.opensecret.maple and .dev; the current .native.dev cannot complete Google/GitHub), a Production profile (today only Local/PR/Release exist and PR still points at app-dev.trymaple.ai), production PCR policy, native Apple audience per lane, associated-domains entitlement + AASA app IDs, App Store Connect tracks (the TestFlight track is separate from the prod and dev tracks today).
  • Apple IAP at Research's level, after the lanes and tracks are unified: StoreKit 2 session (appAccountToken, purchase, acknowledge-then-finish, Transaction.updates listener, unfinished/current-entitlement recovery, 15/30/60/120/300 s retry ladder, sign-out and deletion suspend), Apple pricing page with the ios_iap_enabled kill switch, Apple purchases recovery, "Manage with Apple", upgrade-prompt CTAs into pricing, the guest dialog's purchase/restore actions. Reference: the vendored tauri-plugin-iap state machine and apps/maple-research/docs/apple-billing-client.md.
  • Release hygiene: version 0.1.0 (8) vs 3.4.4, NSAllowsLocalNetworking out of Release, the privacy-manifest reason gap from the 2026-10-11 app-core review (code-docs), CI (macos-26-xlarge is gone, inline Xcode selectors are banned by test_select_xcode.py), the four shared-file rebase conflicts (AGENTS.md, validate-maple/SKILL.md, change_detection.py, flake.nix).

3. Passing on (found, decided not to do)

Entry page and method-first auth structure; /verify/<code> universal link (Research iOS does not claim it; D8 server work stays separate); browser-side auth screens; post-OAuth carry-over (selected_plan, next, code); payment universal links and /pricing arrival (web checkouts cannot start on iOS); redeem, promotions, Zaprite dialog, team seat purchase, invite acceptance route; marketing About page and downloads; fullscreen composer toggle; Escape shortcut; catalog fallback to /v1/models; reasoning summary fallback (the server never emits it); per-paragraph bidi; short-landscape layout rules (SwiftUI adapts, verify on device); highlighter fidelity (D24); loudness normalisation and taper correction (D17: device listening test first); Lockdown Mode copy; pinned drain past 100; the 20-item selection cap; long-press-to-select; the overlay sidebar and two-row header; back-to-exact-chat from Settings.

4. Open UI-convention calls (Mark and Michael, not blocking)

Theme device-wide with anti-flash vs per account; the Settings "Default model" row Research lacks; one delete-all confirmation style (Settings uses two buttons, the wordmark menu asks to type DELETE; Research uses two buttons); visible settings gear vs the wordmark menu; auto-clearing transient chat errors after 3 s; in-app ASWebAuthenticationSession vs Safari with a persisted 15-minute attempt (the Auth contract works with both); "Anonymous" (Research) vs "Guest" (Swift) wording.

5. Decisions recorded 2026-10-13

  • iOS 26 minimum stays (D5 confirmed).
  • No starter code anywhere in the Swift app or SDK.
  • Poll like Research (open conversation incl. idle, recents, title).
  • TTS default 1.2× as on Research.
  • Email sign-up name "ANON", no Name field.
  • Thinking control lives in Mark's options sheet.
  • Pinned chats appear inside their folder as on Research.
  • No capability gates Research does not have.
  • Guests, team settings/invites, Swift-native documents/OCR, the document chip and the verification gate are in scope (D9, D15, D16 superseded); IAP purchase surfaces and lane/track correctness are merge blockers handled last (D4/D7/D12/D30/D31).
  • Everything in §3 is passed on deliberately.

@AnthonyRonning

Copy link
Copy Markdown
Contributor Author

I checked the options-sheet/Menu report independently with native touches and a regression test. It is real on the untouched base; #1153 now restores the bar after both scrim and swipe dismissal. Edge navigation could recover the old state, so "until relaunch" was stronger than the reproduction supported.

One extra simulator testing limitation remains: the opt-in Free image-menu check gets an Application-only XCTest accessibility tree when the native menu opens. Direct interaction still reaches Photo library and the paid-plan Images notice. I left the product UI unchanged and recorded that failed automation separately in #1153. OCR testing was deferred as requested.

@AnthonyRonning

Copy link
Copy Markdown
Contributor Author

Merged #1153 into codex-ios-native-swift at 6baf139ca (tested parity head 09fcb3359). The round-two fixes are included.

Web search now defaults on for all plans, including older preferences with a missing/null field. An explicit saved off choice stays off, and there is no client plan/model gate on web search.

I also extended the native dismissal reconciliation to the queue, media notice and paste sheets, plus the shared camera/image cover path. The hook runs at native dismissal completion even if the logical presentation lease was already released. This keeps the existing ownership guards and avoids clearing another presentation’s lease.

Validation: 475 app tests passed with OCR excluded, strict formatting passed, and the unsigned simulator, iOS simulator/device package cross-builds and Local runtime build passed. Four native iPhone UI tests passed: options scrim/swipe dismissal, queue Done/swipe dismissal with draft/queue owners retained, image-cover dismissal followed by direct Menu/Close, and covered-completion eligibility. The unchanged SDK has its earlier 120-test and release-build pass. The user also reported that manual smoke testing looked good.

Camera hardware, actual paste input and Photo/file picker completion remain unverified; OCR is deferred to the user’s phone. The earlier Free iPad image-menu automation still failed because XCTest exposed an empty hierarchy, although manual simulator interaction reached the expected Images notice. Full evidence and source references are in #1153.

Bring the native Swift chat, composer and library closer to Maple Research:
thinking controls and queued effort capture, transcript/recents refresh,
per-item history anchors, legacy/tool rendering, native paste and list
continuation, running/unread state, project detail and library actions.

Keep the plan/usage summary in Settings under Usage. Clear stale pause state
when an empty queue starts a fresh direct run, preserving explicit pause for
pending work and edits.

Validation: Swift SDK 122/122; app 597/598 with the unchanged Vision OCR
baseline failure; canonical simulator and device/simulator release builds;
phone/iPad UI checks and manual Local-stack smoke. Detailed evidence and
remaining inherited behaviors are recorded in PR #1155.
@AnthonyRonning

Copy link
Copy Markdown
Contributor Author

Merged parity pass 2 (#1155) into this branch as one squash commit, 71a6e506. Your branch was still at our fork point (6baf139c), and the merged tree matches the tested head exactly.

This adds thinking-level selection and capture for queued messages, transcript/recents polling, per-item reading-position restoration for older pages, legacy document/thinking rendering and tool details, list continuation and native paste, running/unread indicators, and project/library navigation and actions. The plan/usage summary now lives in Settings → Usage. We also fixed the inherited empty-queue pause latch: after Stop, reload or reconnect, a fresh send can start a new queue; messages already waiting still require Resume.

Validation: Swift SDK 122/122; app 597/598, with only the known unchanged Vision OCR test failure; device/simulator release compilation and unsigned simulator build passed. The queue checks passed 5/5 on each device, and the final Settings/navigation checks passed 4/4 on each. Anthony also ran the Local simulator smoke. Detailed evidence is in #1155. CompactMenuNavigation.swift is unchanged.

A few inherited follow-ups we left for later:

  • Image selections exceeding the remaining slots reject the whole batch. With 8 images attached, selecting 3 more rejects all 3; Research attaches the 2 that fit. The existing admission guard is unchanged.
  • Project detail uses 100 chats per page; Research uses 20. We kept the existing shared library paging. This is a paging parity difference, with no observed data loss.
  • The dense-screenshot/OCR test still fails with Vision .nilError in this VM. The test is byte-identical to your base. We left it intact, and physical-phone OCR validation is still deferred; this does not establish a device OCR defect.

This branch has not been deployed

No deployments
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