[orchestrator-v2] fix(mobile): Stabilize iOS draft attachments, Hermes sort, and Home#3923
Conversation
…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>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
ApprovabilityVerdict: 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. |
2857eb3 to
76a88a0
Compare
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.
76a88a0 to
6e9701d
Compare
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.
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.
33a612d to
10513f1
Compare
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.
10513f1 to
a5467d3
Compare
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.
a5467d3 to
0a2efa9
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 0a2efa9. Configure here.
| @@ -0,0 +1,3 @@ | |||
| { | |||
| "expo": {} | |||
| } | |||
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 0a2efa9. Configure here.
There was a problem hiding this comment.
🟠 High
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.
| Events( | ||
| "onComposerChange", | ||
| "onComposerSelectionChange", | ||
| "onComposerFocus", | ||
| "onComposerBlur", | ||
| "onComposerPasteImages", | ||
| "onComposerContentSizeChange", | ||
| ) |
There was a problem hiding this comment.
🟡 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
ComposerEditordoes not destructure or forward the publiconSubmitcallback to the Android native view. Existing callers such asThreadComposerpassonSubmit={handleSend}, so the documented hardware-keyboard submit shortcut is silently ignored on Android, unlike the iOS implementation which forwardsonComposerSubmit={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(() => |
There was a problem hiding this comment.
🟡 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
saveConnectionperforms an unprotected read-modify-write of the entireCONNECTIONS_KEYvalue (andclearSavedConnectiondoes 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) { |
There was a problem hiding this comment.
🟡 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() |
There was a problem hiding this comment.
🟡 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 && |
There was a problem hiding this comment.
🟡 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({ |
There was a problem hiding this comment.
🟡 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
lastSignatureRefskipsnavigation.setOptionsbased on a lossy signature:buildNativeStackOptionsSignaturecovers only a small subset ofNativeStackNavigationOptionsand replaces functions inside options such asheaderSearchBarOptionswith"[fn]". If an omitted option changes, or a caller supplies a new search callback with otherwise identical options (asThreadNavigationSidebardoes), 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.
| 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) { |
There was a problem hiding this comment.
🟡 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 }); | |||
There was a problem hiding this comment.
🟡 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.
There was a problem hiding this comment.
🟡 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.


Summary
Array.prototype.toSortedwith a shared copy-sort helper.PreventRemoveProvider.archivedAtso archived threads do not stick on the Home list after a missed archive delta.Base
This branch is now built on
mwolson/t3codev2-base, a merge of the shared iOS/mobile fixes line (ios-fixes-main) intot3code/codex-turn-mapping. Because GitHub diffs againstt3code/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
idand adataUrl. The start-turn path did not treat those dual-tagged attachments as uploads, so order and persistence broke for mobile screenshots.Array.prototype.toSorted. CTM queue UI calls it fromderiveThreadQueueWorkflowState/ThreadQueueControl, so queuing a reply or finishing a turn can crash withTypeError: undefined is not a function.@t3tools/shared/ArraycopySorted([...values].sort) and use it on mobile-reachable call sites (threadWorkflows,ThreadRelationshipsBanner,use-thread-selection, CTMmodel.ts).lineage.relationshipToParent === "subagent"when grouping Home rows; keep fork threads.setOptionsand could hit maximum update depth throughPreventRemoveProvideron CTM device launches.thread.archivedshell 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.normalizeShellThreadMembershipand apply it to every full snapshot (reducer enrichment refreshes and reconnect heals) so active/archive membership always derives fromarchivedAt. The HTTP heal, forced socket snapshot, and stale-snapshot rejection it pairs with live inv2-base.Validation
Arraytests;shell-sync/shellReducermembership tests.vp check(0 errors) and fullvp run typecheckon the rebuilt branch.com.mwolson.t3code.v2.devfrom local orchestrator-v2 integration; queued reply send after active turn finished without crash; Home list usable after nav stability fix.Notes
v2-base: (1) attachments / Hermes / Home, (2) shell archive membership normalization.t3code/codex-turn-mappingonly. Main-line ios-on-main (fix(mobile): Harden iOS reconnect, crash capture, and chat layout on main #3910) does not haveThreadQueueControl/ this queue path.Note
Stabilize iOS draft attachments, Hermes sort, and Android home screen for mobile
DraftComposerImageAttachmentobjects throughtoUploadChatImageAttachmentsbefore sending, stripping client-only fields (id,previewUri); fixes attachment persistence inpersistAttachmentsto correctly remap only uploaded items in-order while preserving stored attachments.Array.prototype.toSortedcalls (unavailable in Hermes) with a newcopySortedutility from@t3tools/shared/Arrayacross thread relationships, model selection, and workflow state.AndroidHomeFabLayoutwith a floating action button for new tasks, a customAndroidHomeHeaderwith environment/sort/grouping menus, and persists collapsed project groups to mobile preferences.threadResumeCompletionMarkercapability; when requested, the client waits for asynchronizedevent before promoting thread status tolive, ensuring clean resume after reconnect.normalizeShellThreadMembership.libghostty-vtvia JNI for terminal state/rendering, addsTerminalCanvasViewwith gesture handling, cursor blink, and selection, and auto-navigates away when a process exits.withIosCrashLogExpo plugin.MobileEnvironmentCacheStorebacked by SQLite, a newMobilePreferencesStorewith optimistic updates, and imperative wrappers for all storage operations; web storage gains server config and VCS refs stores.AndroidScreenHeader,AppSymbol(Tabler icon mapping),ControlPillMenuwith anchored menu,ConfirmDialogHost, andOverlayPortal; applies platform-specific headers across terminal, review, files, settings, home, and thread screens.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-vtterminal backend (vendored libs, build script, and docs). iOS gets durable crash logging (installCrashLog, nativeT3CrashLog/ sync writes) and a markdown layout fix that rebuilds attributed content inlayout()so text does not disappear when Yoga skipsmeasureContent.Build & config: Optional
T3CODE_IOS_PERSONAL_TEAMflow with custom bundle id validation, stripped widget/Sign in with Apple/push viawithoutIosPersonalTeamCapabilities, and gated ClerkappleSignIn. Registers DM Sans consistently acrossapp.config.tsandglobal.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 EASNODE_OPTIONSheap. README documents Personal Team andios: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.