Skip to content

[orchestrator-v2] fix(mobile): Stabilize iOS draft attachments, Hermes sort, and Home#3923

Open
mwolson wants to merge 33 commits into
pingdotgg:t3code/codex-turn-mappingfrom
mwolson:fix/ios-v2-attachment-persistence
Open

[orchestrator-v2] fix(mobile): Stabilize iOS draft attachments, Hermes sort, and Home#3923
mwolson wants to merge 33 commits into
pingdotgg:t3code/codex-turn-mappingfrom
mwolson:fix/ios-v2-attachment-persistence

Conversation

@mwolson

@mwolson mwolson commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Keep CTM mobile draft image attachments working when they carry both a local id and a dataUrl.
  • Avoid a Hermes fatal in the queue UI by replacing missing Array.prototype.toSorted with a shared copy-sort helper.
  • Hide orchestration-v2 subagent threads from the mobile Home list (forks stay visible).
  • Stabilize Home native-stack options and callbacks so ordinary Home updates do not re-enter navigation through PreventRemoveProvider.
  • Normalize V2 shell active/archive membership from archivedAt so archived threads do not stick on the Home list after a missed archive delta.

Base

This branch is now built on mwolson/t3code v2-base, a merge of the shared iOS/mobile fixes line (ios-fixes-main) into t3code/codex-turn-mapping. Because GitHub diffs against t3code/codex-turn-mapping, the PR diff includes that merge (mobile keyboard/feed fixes, shell reconnect heal, serverConfig/vcsRefs caching, and the V2 thread resume completion marker) in addition to the two commits listed below. Review the two head commits for this PR's own delta.

Problem and Fix

Problem and Why it Happened Fix
CTM drafts can include image attachments with both a local id and a dataUrl. The start-turn path did not treat those dual-tagged attachments as uploads, so order and persistence broke for mobile screenshots. Treat dual-tagged attachments as uploads, strip local-only fields before persistence, and dispatch server IDs while preserving order with already-persisted attachments.
Hermes 250829098 (RN 0.85.3) does not implement Array.prototype.toSorted. CTM queue UI calls it from deriveThreadQueueWorkflowState / ThreadQueueControl, so queuing a reply or finishing a turn can crash with TypeError: undefined is not a function. Add @t3tools/shared/Array copySorted ([...values].sort) and use it on mobile-reachable call sites (threadWorkflows, ThreadRelationshipsBanner, use-thread-selection, CTM model.ts).
Orchestration-v2 subagent shells appeared in the mobile Home list, unlike the desktop sidebar. Skip threads with lineage.relationshipToParent === "subagent" when grouping Home rows; keep fork threads.
Unstable Home native-stack options and callbacks re-entered setOptions and could hit maximum update depth through PreventRemoveProvider on CTM device launches. Memoize iOS Home header options (filter menu inputs as deps; handlers via ref) and stabilize Settings / New Task callbacks and title option objects.
A dropped thread.archived shell delta (or a mis-tagged full snapshot) could keep an archived thread listed as active on Home even after the reconnect heal applied a fresh snapshot. Add normalizeShellThreadMembership and apply it to every full snapshot (reducer enrichment refreshes and reconnect heals) so active/archive membership always derives from archivedAt. The HTTP heal, forced socket snapshot, and stale-snapshot rejection it pairs with live in v2-base.

Validation

  • Targeted tests: client-runtime attachment command tests; Home subagent filter tests (including fork negative control); shared Array tests; shell-sync / shellReducer membership tests.
  • vp check (0 errors) and full vp run typecheck on the rebuilt branch.
  • Device: archived threads from a paired CTM environment leave Home after reconnect/heal.
  • Device: Release Hermes com.mwolson.t3code.v2.dev from local orchestrator-v2 integration; queued reply send after active turn finished without crash; Home list usable after nav stability fix.

Notes

Note

Stabilize iOS draft attachments, Hermes sort, and Android home screen for mobile

  • iOS draft attachments: Normalizes DraftComposerImageAttachment objects through toUploadChatImageAttachments before sending, stripping client-only fields (id, previewUri); fixes attachment persistence in persistAttachments to correctly remap only uploaded items in-order while preserving stored attachments.
  • Hermes sort compatibility: Replaces Array.prototype.toSorted calls (unavailable in Hermes) with a new copySorted utility from @t3tools/shared/Array across thread relationships, model selection, and workflow state.
  • Android home screen: Adds AndroidHomeFabLayout with a floating action button for new tasks, a custom AndroidHomeHeader with environment/sort/grouping menus, and persists collapsed project groups to mobile preferences.
  • Thread resume completion marker: Server now advertises threadResumeCompletionMarker capability; when requested, the client waits for a synchronized event before promoting thread status to live, ensuring clean resume after reconnect.
  • Shell sync hardening: Disk cache is no longer painted immediately on startup; HTTP heal or a forced full socket snapshot is required before resuming deltas; archived thread membership is normalized via normalizeShellThreadMembership.
  • Android native terminal: Integrates libghostty-vt via JNI for terminal state/rendering, adds TerminalCanvasView with gesture handling, cursor blink, and selection, and auto-navigates away when a process exits.
  • Crash logging infrastructure: Installs a global crash logger at startup that persists crash records and breadcrumbs to disk; native iOS fatal handlers are injected via the withIosCrashLog Expo plugin.
  • Persistence layer: Introduces MobileEnvironmentCacheStore backed by SQLite, a new MobilePreferencesStore with optimistic updates, and imperative wrappers for all storage operations; web storage gains server config and VCS refs stores.
  • Android UI parity: Adds AndroidScreenHeader, AppSymbol (Tabler icon mapping), ControlPillMenu with anchored menu, ConfirmDialogHost, and OverlayPortal; applies platform-specific headers across terminal, review, files, settings, home, and thread screens.
  • Risk: Fonts are no longer asynchronously loaded before the splash screen hides; any component relying on font load completion before first render will use the system fallback briefly.

Macroscope summarized 0a2efa9.


Note

High Risk
Large new native Android/iOS surface (custom views, vendored terminal binaries, entitlements/plugins) increases build and runtime regression risk across platforms.

Overview
Expands the Expo mobile app with first-class Android implementations for the composer editor, review diff surface, and header controls, plus an Android Ghostty libghostty-vt terminal backend (vendored libs, build script, and docs). iOS gets durable crash logging (installCrashLog, native T3CrashLog / sync writes) and a markdown layout fix that rebuilds attributed content in layout() so text does not disappear when Yoga skips measureContent.

Build & config: Optional T3CODE_IOS_PERSONAL_TEAM flow with custom bundle id validation, stripped widget/Sign in with Apple/push via withoutIosPersonalTeamCapabilities, and gated Clerk appleSignIn. Registers DM Sans consistently across app.config.ts and global.css, adds expo-sqlite, expo-quick-actions, expo-asset, Android Gradle/heap/popup/alert/predictive-back plugins, enables predictive back, blocks Metro from watching .t3, and bumps EAS NODE_OPTIONS heap. README documents Personal Team and ios:release; marketing shows Ctrl⏎ on non-Mac.

Smaller repo touches: Documents Effect service dependency/runtime boundary rules in check-run agents; bumps electron-builder; adds minimal root app.json.

Reviewed by Cursor Bugbot for commit 0a2efa9. Bugbot is set up for automated code reviews on this repo. Configure here.

juliusmarminge and others added 17 commits July 7, 2026 15:55
…gg#3781)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
…pingdotgg#3795)

Co-authored-by: Julius Marminge <julius@mac.lan>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
…tgg#3823)

Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: Horus Lugo <horusgoul@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: maria-rcks <maria@kuuro.net>
Co-authored-by: Shivam Sharma <91240327+shivamhwp@users.noreply.github.com>
Co-authored-by: Ben Davis <45952064+bmdavis419@users.noreply.github.com>
Co-authored-by: Alex <me@pixp.cc>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: Julius Marminge <julius@mac.lan>
Co-authored-by: codex <codex@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. 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

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 8f1465a3-b5ef-4a84-9912-9f08c4c69cd4

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jul 12, 2026
Comment thread apps/mobile/src/features/home/HomeHeader.tsx
@mwolson mwolson marked this pull request as ready for review July 12, 2026 21:35
Comment thread packages/shared/src/Array.ts Outdated
@macroscopeapp

macroscopeapp Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

15 blocking correctness issues found. Diff is too large for automated approval analysis. A human reviewer should evaluate this PR.

You can customize Macroscope's approvability policy. Learn more.

@mwolson mwolson force-pushed the fix/ios-v2-attachment-persistence branch from 2857eb3 to 76a88a0 Compare July 12, 2026 21:41
@mwolson mwolson changed the title fix(mobile): Stabilize CTM iOS draft attachments, Hermes sort, and Home [orchestrator-v2] fix(mobile): Stabilize CTM iOS draft attachments, Hermes sort, and Home Jul 12, 2026
mwolson added 2 commits July 12, 2026 18:53
Capture RCTFatal message and stack to Documents via a native handler so
Release Hermes aborts leave last-crash.json without a device console.
Harden the JS ErrorUtils path with a minimal write-first record, durable
fsync writes, breadcrumb max-wait flush, and an Expo AppDelegate plugin.

Skip NativeStackScreenOptions setOptions when content is unchanged so
Thread catch-up re-renders do not loop through PreventRemoveProvider and
hit maximum update depth.
CI Mobile Native Static Analysis failed on a multi-line if where the
opening brace was on the following line.
Warm-cache afterSequence resume can skip a dropped thread.archived
delta after later events advance snapshotSequence, leaving archived
threads on the home list. Always HTTP-heal (or force a full socket
snapshot) before resuming live shell deltas, and reject stale full
snapshots from the stream.
Comment thread packages/client-runtime/src/state/shell.ts Outdated
When HTTP shell heal fails, the full socket snapshot is the membership
source of truth. Accept that first snapshot even if its sequence is behind
the warm disk cache so ghost active threads cannot stick.
@mwolson mwolson force-pushed the fix/ios-v2-attachment-persistence branch from 33a612d to 10513f1 Compare July 13, 2026 02:19
Comment thread packages/client-runtime/src/state/shell.ts
Comment thread packages/client-runtime/src/state/shell.ts Outdated
A prior session can arm acceptNextSocketSnapshotAuthoritatively when HTTP
heal fails and then disconnect before the socket snapshot arrives. Clear
the flag on a successful HTTP heal so a later reconnect does not treat
stale enrichment snapshots as authoritative.
@mwolson mwolson force-pushed the fix/ios-v2-attachment-persistence branch from 10513f1 to a5467d3 Compare July 13, 2026 02:27
@mwolson mwolson changed the title [orchestrator-v2] fix(mobile): Stabilize CTM iOS draft attachments, Hermes sort, and Home [orchestrator-v2] fix(mobile): Stabilize iOS draft attachments, Hermes sort, and Home Jul 13, 2026
mwolson and others added 11 commits July 12, 2026 22:40
waitForJsonLogMatch only yieldNow'd between attempts, so under busy CI it
could return before the ACP mock agent wrote session/cancel. Use a real
wall-clock delay (not TestClock sleep) and fail explicitly on timeout.
CI Check treats setTimeout as an Effect lint error. Keep wall-clock delays
via a diagnostics-scoped helper, fail with a tagged timeout, and avoid
TestClock-bound Effect.sleep so ACP mock log polls still work under load.
Move the header options memo before the missing environmentId, threadId,
or selectedThread guards so hook order stays stable if those become null
on a re-render of a mounted ThreadRouteContent instance.
## Summary

- Add a synchronized stream item after a resumed thread subscription has attached its live buffer.
- Keep a warm cached thread synchronizing through reconnect until that item arrives.

## Notes

Squash-merge of twistedgrim's PR onto ios-fixes-main. Follow-up gate commit will land separately with capability negotiation and marker-after-catch-up ordering.
Emit the completion marker only on subscribeThread when clients opt in,
after catch-up replay, and keep warm threads synchronizing until it
arrives. Advertise support via ServerConfig so old servers still use the
ready fallback and old clients never see an unknown stream kind.
Do not paint a warm disk shell snapshot before the HTTP or socket heal.
Stale active membership was flashing a larger Home list, then shrinking
when the authoritative heal arrived. Disk remains offline/heal fallback.
Keep Home empty while session is still None during online connect setup;
only apply disk fallback from true disconnect. Persist markerMode so
thread resume re-arms awaitingCompletion after ordinary reconnects.
…Android

Anchor the chat scroll position to the end of the scroll range when the
keyboard opens with the list resting at the bottom, reading the live
composer padding per frame. This survives the event-ordering race where
composer expansion lands before the keyboard's onStart (quick tap after
opening a thread), which zeroed out the previous delta-based
compensation and left the last message hidden behind the input. Also
re-assert the settled scroll target after the keyboard animation ends
and guard that correction with shouldShiftContent.
…-base

Cut B0 for the merge-based orchestrator-v2 build model: an append-only
merge of ios-fixes-main (924ad10) and upstream/t3code/codex-turn-mapping
(c14c908). Notable resolutions:

- Adopt the V2 orchestration surface (client-runtime threads/shell state,
  server orchestration-v2) while preserving v1 fixes: shell reconnect and
  status logic, serverConfig/vcsRefs caching, the mobile storage refactor,
  and the mobile thread UI layer (AppSymbol mappings, ThreadFeed refs).
- Port the full thread resume completion marker (requestCompletionMarker
  plus synchronized stream item) onto the V2 contracts, server, and ws
  surfaces; drop the dead v1-only marker schema since CTM removed the v1
  subscribe path.
- Restore expectedBranch on ThreadMetaUpdateCommand (live consumers in
  the server decider and web GitActionsControl).
- Adapt web and mobile environment caches to the shared V2 cache schema
  (ORCHESTRATION_CACHE_SCHEMA_VERSION, projection.thread.id keys,
  decode-or-discard on corrupt records).
- Regenerate pnpm-lock.yaml from CTM's side with the keyboard patch
  intact.
Persist dual-tagged draft image attachments, replace Hermes-missing toSorted
with copySorted on mobile-reachable paths, hide subagent threads from Home,
and stabilize Home native-stack options to avoid PreventRemove max-update-depth.
Derive active/archive membership from archivedAt when applying full shell
snapshots (reducer enrichment refreshes and reconnect heals) so a dropped
archive delta cannot stick an archived thread on the home list. The HTTP
heal, forced socket snapshot, and stale-snapshot rejection this pairs with
live in v2-base.
@mwolson mwolson force-pushed the fix/ios-v2-attachment-persistence branch from a5467d3 to 0a2efa9 Compare July 14, 2026 03:13
@github-actions github-actions Bot added size:XXL 1,000+ changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Jul 14, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0a2efa9. Configure here.

} else {
Typeface.DEFAULT
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Android composer ignores font family

Medium Severity

setFontFamily only switches between Typeface.MONOSPACE and Typeface.DEFAULT. Names like DMSans-Regular from app.config.ts never load, so the Android composer keeps the system default while the rest of the app uses DM Sans.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0a2efa9. Configure here.

Comment thread app.json
@@ -0,0 +1,3 @@
{
"expo": {}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Empty root Expo config committed

Low Severity

A new repo-root app.json with an empty "expo": {} object was added. Mobile already configures Expo via apps/mobile/app.config.ts, and the PR notes call out excluding local scaffolding, so this looks like accidental tooling output that can confuse monorepo Expo resolution.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0a2efa9. Configure here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 High

if (nextSnapshot === null) {

applyItem checks hasServerBackedSnapshot and current.snapshot at the top of the fromDisk path but the check is not atomic with the state update at the bottom. If setDisconnected starts applyDiskFallback concurrently with an HTTP heal, the disk fiber can pass the guard, yield, then resume after the HTTP fiber installs its authoritative snapshot — and overwrite it with the stale cached snapshot, reintroducing archived threads that the heal removed. The fromDisk guard needs to be rechecked immediately before committing the disk snapshot (or serialized with the state update) so a concurrent authoritative heal wins.

🤖 Copy this AI Prompt to have your agent fix this:
In file @packages/client-runtime/src/state/shell.ts around line 138:

`applyItem` checks `hasServerBackedSnapshot` and `current.snapshot` at the top of the `fromDisk` path but the check is not atomic with the state update at the bottom. If `setDisconnected` starts `applyDiskFallback` concurrently with an HTTP heal, the disk fiber can pass the guard, yield, then resume after the HTTP fiber installs its authoritative snapshot — and overwrite it with the stale cached snapshot, reintroducing archived threads that the heal removed. The `fromDisk` guard needs to be rechecked immediately before committing the disk snapshot (or serialized with the state update) so a concurrent authoritative heal wins.

Comment on lines +52 to +59
Events(
"onComposerChange",
"onComposerSelectionChange",
"onComposerFocus",
"onComposerBlur",
"onComposerPasteImages",
"onComposerContentSizeChange",
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium t3composereditor/T3ComposerEditorModule.kt:52

Android hardware-key submit does not work: T3ComposerEditorModule omits the onComposerSubmit event and T3ComposerEditorView has no hardware-key handler, so pressing the documented submit shortcut on Android never emits a submit event or invokes handleSend, unlike iOS which emits onComposerSubmit for Command-Return. Register an onComposerSubmit event in the Events(...) list and add an OnKeyListener (or equivalent) on the editor that emits it for the Android submit shortcut.

      Events(
        "onComposerChange",
        "onComposerSelectionChange",
        "onComposerFocus",
        "onComposerBlur",
        "onComposerPasteImages",
+        "onComposerSubmit",
        "onComposerContentSizeChange",
      )
Also found in 1 other location(s)

apps/mobile/src/native/T3ComposerEditor.native.tsx:87

ComposerEditor does not destructure or forward the public onSubmit callback to the Android native view. Existing callers such as ThreadComposer pass onSubmit={handleSend}, so the documented hardware-keyboard submit shortcut is silently ignored on Android, unlike the iOS implementation which forwards onComposerSubmit={onSubmit}.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorModule.kt around lines 52-59:

Android hardware-key submit does not work: `T3ComposerEditorModule` omits the `onComposerSubmit` event and `T3ComposerEditorView` has no hardware-key handler, so pressing the documented submit shortcut on Android never emits a submit event or invokes `handleSend`, unlike iOS which emits `onComposerSubmit` for Command-Return. Register an `onComposerSubmit` event in the `Events(...)` list and add an `OnKeyListener` (or equivalent) on the editor that emits it for the Android submit shortcut.

Also found in 1 other location(s):
- apps/mobile/src/native/T3ComposerEditor.native.tsx:87 -- `ComposerEditor` does not destructure or forward the public `onSubmit` callback to the Android native view. Existing callers such as `ThreadComposer` pass `onSubmit={handleSend}`, so the documented hardware-keyboard submit shortcut is silently ignored on Android, unlike the iOS implementation which forwards `onComposerSubmit={onSubmit}`.

Effect.forkScoped,
);

yield* Effect.addFinalizer(() =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium state/server.ts:172

The finalizer at line 172 saves the pending config, but it runs before the persistence worker fiber is interrupted because Effect finalizers execute in reverse registration order. When the scope closes, the finalizer can write config B while the worker is still writing an older config A; if the worker's write finishes after the finalizer's, it overwrites the cache with stale data. Join or stop the worker before flushing in the finalizer, or serialize both paths through the same lane.

Also found in 1 other location(s)

apps/mobile/src/persistence/mobile-storage.ts:167

saveConnection performs an unprotected read-modify-write of the entire CONNECTIONS_KEY value (and clearSavedConnection does the same). If two connection saves, or a save and clear, run concurrently, both can read the same old list and the last write silently discards the other operation, losing a saved connection or resurrecting a cleared one. These operations need to share a lock around the complete read/write transaction.

🤖 Copy this AI Prompt to have your agent fix this:
In file @packages/client-runtime/src/state/server.ts around line 172:

The finalizer at line 172 saves the pending config, but it runs *before* the persistence worker fiber is interrupted because Effect finalizers execute in reverse registration order. When the scope closes, the finalizer can write config B while the worker is still writing an older config A; if the worker's write finishes after the finalizer's, it overwrites the cache with stale data. Join or stop the worker before flushing in the finalizer, or serialize both paths through the same lane.

Also found in 1 other location(s):
- apps/mobile/src/persistence/mobile-storage.ts:167 -- `saveConnection` performs an unprotected read-modify-write of the entire `CONNECTIONS_KEY` value (and `clearSavedConnection` does the same). If two connection saves, or a save and clear, run concurrently, both can read the same old list and the last write silently discards the other operation, losing a saved connection or resurrecting a cleared one. These operations need to share a lock around the complete read/write transaction.

(materialName ? ANDROID_ICON_BY_MATERIAL_NAME[materialName] : undefined) ??
(sfSymbol ? ANDROID_ICON_BY_SF_SYMBOL[sfSymbol] : undefined);

if (!AndroidIcon) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium components/AppSymbol.tsx:190

SymbolView renders nothing on Android for SF symbol names that are missing from ANDROID_ICON_BY_SF_SYMBOL, and callers pass no fallback. Several reachable call sites use names that are omitted from the map — for example arrow.down on the queue move-down button, plus list.number, hourglass, arrow.counterclockwise, and arrow.up.right.square — so those icons disappear on Android, including an actionable queue control. The fallback branch at line 190–192 returns props.fallback ?? null, which is null for these callers. Add the missing SF-symbol-to-Tabler mappings for all call sites (or supply a generic fallback) so every symbol used through the wrapper has an Android glyph.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/components/AppSymbol.tsx around line 190:

`SymbolView` renders nothing on Android for SF symbol names that are missing from `ANDROID_ICON_BY_SF_SYMBOL`, and callers pass no `fallback`. Several reachable call sites use names that are omitted from the map — for example `arrow.down` on the queue move-down button, plus `list.number`, `hourglass`, `arrow.counterclockwise`, and `arrow.up.right.square` — so those icons disappear on Android, including an actionable queue control. The fallback branch at line 190–192 returns `props.fallback ?? null`, which is `null` for these callers. Add the missing SF-symbol-to-Tabler mappings for all call sites (or supply a generic fallback) so every symbol used through the wrapper has an Android glyph.

private var horizontalOffset = 0
private var lastVisibleRange: Pair<Int, Int>? = null

var rows: List<DiffRow> = emptyList()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium t3reviewdiff/T3ReviewDiffView.kt:583

When rows is replaced with a new list that occupies the same index range as the current viewport (e.g. a same-sized diff at offset 0), onVisibleRowsChanged is not emitted for the new content. The rows setter calls rebuildOffsets() but never clears lastVisibleRange, so emitVisibleRange() still sees the old (first, last) pair, treats it as already-emitted, and suppresses the callback. Consider resetting lastVisibleRange = null in the rows setter so the next invalidate() re-emits the visible range for the new content.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/modules/t3-review-diff/android/src/main/java/expo/modules/t3reviewdiff/T3ReviewDiffView.kt around line 583:

When `rows` is replaced with a new list that occupies the same index range as the current viewport (e.g. a same-sized diff at offset 0), `onVisibleRowsChanged` is not emitted for the new content. The `rows` setter calls `rebuildOffsets()` but never clears `lastVisibleRange`, so `emitVisibleRange()` still sees the old `(first, last)` pair, treats it as already-emitted, and suppresses the callback. Consider resetting `lastVisibleRange = null` in the `rows` setter so the next `invalidate()` re-emits the visible range for the new content.

parsed?.connections ?? [],
Arr.filter(
(connection) =>
!!connection.environmentId &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium persistence/mobile-storage.ts:157

loadSavedConnections accesses connection.environmentId on each array element without first checking that the element is an object. A persisted payload like {"connections":[null]} throws a TypeError while loading connections, instead of ignoring the malformed entry. Since saveConnection and clearSavedConnection both read the current list before writing, this crash also blocks any save or clear operation once a null element is present in storage. Add a guard (e.g. connection != null && typeof connection === "object") inside the Arr.filter predicate before dereferencing properties.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/persistence/mobile-storage.ts around line 157:

`loadSavedConnections` accesses `connection.environmentId` on each array element without first checking that the element is an object. A persisted payload like `{"connections":[null]}` throws a `TypeError` while loading connections, instead of ignoring the malformed entry. Since `saveConnection` and `clearSavedConnection` both read the current list before writing, this crash also blocks any save or clear operation once a `null` element is present in storage. Add a guard (e.g. `connection != null && typeof connection === "object"`) inside the `Arr.filter` predicate before dereferencing properties.

return "";
}
const record = options as Record<string, unknown>;
return stableJsonStringify({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium native/nativeStackOptionsSignature.ts:12

buildNativeStackOptionsSignature only hashes a fixed allowlist of fields, so changes to any other NativeStackScreenOptions property (e.g. headerShown changing from true to false) produce an identical signature. The caller then skips setOptions, leaving the screen with a stale navigation configuration. Include every forwarded option in the structural comparison, applying special handling only where needed.

Also found in 1 other location(s)

apps/mobile/src/native/StackHeader.tsx:114

lastSignatureRef skips navigation.setOptions based on a lossy signature: buildNativeStackOptionsSignature covers only a small subset of NativeStackNavigationOptions and replaces functions inside options such as headerSearchBarOptions with &#34;[fn]&#34;. If an omitted option changes, or a caller supplies a new search callback with otherwise identical options (as ThreadNavigationSidebar does), the signature remains unchanged and navigation retains the previous value/closure. This can leave screen configuration stale and dispatch search changes to an obsolete callback. Either include all option values with callback-safe wrappers, or limit this deduplication to the specific stabilized factories.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/native/nativeStackOptionsSignature.ts around line 12:

`buildNativeStackOptionsSignature` only hashes a fixed allowlist of fields, so changes to any other `NativeStackScreenOptions` property (e.g. `headerShown` changing from `true` to `false`) produce an identical signature. The caller then skips `setOptions`, leaving the screen with a stale navigation configuration. Include every forwarded option in the structural comparison, applying special handling only where needed.

Also found in 1 other location(s):
- apps/mobile/src/native/StackHeader.tsx:114 -- `lastSignatureRef` skips `navigation.setOptions` based on a lossy signature: `buildNativeStackOptionsSignature` covers only a small subset of `NativeStackNavigationOptions` and replaces functions inside options such as `headerSearchBarOptions` with `"[fn]"`. If an omitted option changes, or a caller supplies a new search callback with otherwise identical options (as `ThreadNavigationSidebar` does), the signature remains unchanged and navigation retains the previous value/closure. This can leave screen configuration stale and dispatch search changes to an obsolete callback. Either include all option values with callback-safe wrappers, or limit this deduplication to the specific stabilized factories.

Comment on lines +37 to +42
if (loaded) return
loaded = true
try {
regular = Typeface.createFromAsset(context.assets, "fonts/MesloLGS-NF-Regular.ttf")
bold = Typeface.createFromAsset(context.assets, "fonts/MesloLGS-NF-Bold.ttf")
} catch (error: RuntimeException) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium t3terminal/TerminalCanvasView.kt:37

If fonts/MesloLGS-NF-Bold.ttf is missing or invalid while the regular asset loads successfully, regular is updated to the Meslo face but bold stays at the default Typeface.MONOSPACE fallback. Because the terminal cell width is derived from the regular face, bold text then renders with mismatched glyph metrics, causing misaligned or overlapping cells. Assign to locals first and publish both only after both loads succeed, or derive bold from the loaded regular face so the metrics stay consistent.

    loaded = true
    try {
-      regular = Typeface.createFromAsset(context.assets, "fonts/MesloLGS-NF-Regular.ttf")
-      bold = Typeface.createFromAsset(context.assets, "fonts/MesloLGS-NF-Bold.ttf")
+      val regularFace = Typeface.createFromAsset(context.assets, "fonts/MesloLGS-NF-Regular.ttf")
+      val boldFace = Typeface.createFromAsset(context.assets, "fonts/MesloLGS-NF-Bold.ttf")
+      regular = regularFace
+      bold = boldFace
    } catch (error: RuntimeException) {
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/TerminalCanvasView.kt around lines 37-42:

If `fonts/MesloLGS-NF-Bold.ttf` is missing or invalid while the regular asset loads successfully, `regular` is updated to the Meslo face but `bold` stays at the default `Typeface.MONOSPACE` fallback. Because the terminal cell width is derived from the regular face, bold text then renders with mismatched glyph metrics, causing misaligned or overlapping cells. Assign to locals first and publish both only after both loads succeed, or derive `bold` from the loaded regular face so the metrics stay consistent.

@@ -296,6 +316,7 @@ function ConfiguredSettingsRouteScreen() {
return;
}

savePreferences({ liveActivitiesEnabled: true });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium settings/SettingsRouteScreen.tsx:319

savePreferences({ liveActivitiesEnabled: true }) on line 319 is called fire-and-forget after setLiveActivityUpdatesEnabled already committed the relay preference. If the local preference save fails, updateMobilePreferencesAtom rolls back its optimistic value — so the UI reverts to "disabled" while the relay stays enabled. The same divergence happens on the disable path (line 403): the relay is disabled but the local preference silently rolls back to enabled. In both cases the switch state and actual delivery behavior become inconsistent. Consider awaiting savePreferences and reverting the relay preference (or surfacing an error) when the local save fails.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/settings/SettingsRouteScreen.tsx around line 319:

`savePreferences({ liveActivitiesEnabled: true })` on line 319 is called fire-and-forget after `setLiveActivityUpdatesEnabled` already committed the relay preference. If the local preference save fails, `updateMobilePreferencesAtom` rolls back its optimistic value — so the UI reverts to "disabled" while the relay stays enabled. The same divergence happens on the disable path (line 403): the relay is disabled but the local preference silently rolls back to enabled. In both cases the switch state and actual delivery behavior become inconsistent. Consider awaiting `savePreferences` and reverting the relay preference (or surfacing an error) when the local save fails.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium

The themeConfig setter calls parseThemeConfig(value) without first resetting cursorColorValue and paletteColors, so changing to a theme that omits cursor-color or palette entries leaves the previous theme's cursor color and palette in effect. Setting themeConfig to "" (or to a config that only changes a subset of colors) renders a mix of old and new theme colors instead of the requested theme. Consider resetting cursorColorValue and paletteColors to their defaults at the start of parseThemeConfig before applying the new config.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt around line 388:

The `themeConfig` setter calls `parseThemeConfig(value)` without first resetting `cursorColorValue` and `paletteColors`, so changing to a theme that omits `cursor-color` or `palette` entries leaves the previous theme's cursor color and palette in effect. Setting `themeConfig` to `""` (or to a config that only changes a subset of colors) renders a mix of old and new theme colors instead of the requested theme. Consider resetting `cursorColorValue` and `paletteColors` to their defaults at the start of `parseThemeConfig` before applying the new config.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants