fix(mobile): compose button no longer covers the last tab on iPhone#32995

merged1
main
fix/issue-32974
opened1d agobyJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 1d ago

Problem

On iPhones with the floating glass tab bar, the compose button beside the workspace tabs can be drawn over a tab (usually Settings) instead of 16pt to the right of the bar. WorkspaceTabBar.layoutSubviews keeps only tab buttons whose converted size exactly equals their bounds size (the check from #28955 that skips the glass lens's magnified copy of the row). Converting a rect through the view hierarchy isn't exact in floating point, so at some bar widths real buttons fail the == comparison. The row then ends early and compose is placed after the wrong tab, or is never placed when no button passes.

Reproduced on current main in the real app (Expo dev client, iPhone 15 Pro simulator, 393pt, iOS 27.2, Chats selected). The stock bar width is correct on that runtime, so the bar width was stepped 1/3pt (one device pixel) at a time through the JS host inset. 16 of 28 widths placed compose wrongly: at a 13.6667pt inset compose covers Settings (the reported symptom), and at 12.3333pt it covers PRs/Usage.

Change

The filter now compares converted and local sizes with a tolerance of less than 1pt on width and height instead of exact equality. That absorbs conversion rounding, while the lens copy (about 1.16×, so roughly 13pt wider and 9pt taller than a real tab button) is still excluded.

Scope and approval

Fixes pingdotgg/t3code#32974

One predicate in one native iOS view; no other behavior changes. The tolerance is the one proposed in the issue.

Verification

  • Native build: the iOS dev client for this head built on EAS (fingerprint 97c1225e); no Swift toolchain on the Linux machine used, so compilation was verified only through that build.
  • vp run check:changed: format and typecheck pass (no TS/JS changes; Swift is not linted there).
  • Width sweep in the real app, iPhone 15 Pro, iOS 27.2, same JS on both sides, native client from main (ade473018d) vs this branch (0727bded22): before 16/28 widths misplace compose, after 0/28.
Width sweep results (compose should be 16pt after Settings)
Right inset (pt)Before: Settings maxX / compose x / gapAfter: Settings maxX / compose x / gap
12.0OK 292.00 / 308.00 / 16.00OK 292.00 / 308.00 / 16.00
12.3333BAD 291.67 / 128.67 / -163.00OK 291.67 / 307.67 / 16.00
12.6667BAD 291.33 / 128.58 / -162.75OK 291.33 / 307.33 / 16.00
13.0OK 291.00 / 307.00 / 16.00OK 291.00 / 307.00 / 16.00
13.3333BAD 290.67 / 128.42 / -162.25OK 290.67 / 306.67 / 16.00
13.6667BAD 290.33 / 247.00 / -43.33OK 290.33 / 306.33 / 16.00
14.0OK 290.00 / 306.00 / 16.00OK 290.00 / 306.00 / 16.00
14.3333BAD 289.67 / 246.50 / -43.17OK 289.67 / 305.67 / 16.00
14.6667BAD 289.33 / 128.08 / -161.25OK 289.33 / 305.33 / 16.00
15.0OK 289.00 / 305.00 / 16.00OK 289.00 / 305.00 / 16.00
15.3333BAD 288.67 / 127.92 / -160.75OK 288.67 / 304.67 / 16.00
15.6667BAD 288.33 / 127.83 / -160.50OK 288.33 / 304.33 / 16.00
16.0OK 288.00 / 304.00 / 16.00OK 288.00 / 304.00 / 16.00
16.3333OK 287.67 / 303.67 / 16.00OK 287.67 / 303.67 / 16.00
16.6667OK 287.33 / 303.33 / 16.00OK 287.33 / 303.33 / 16.00
17.0OK 287.00 / 303.00 / 16.00OK 287.00 / 303.00 / 16.00
17.3333BAD 286.67 / 127.42 / -159.25OK 286.67 / 302.67 / 16.00
17.6667BAD 286.33 / 127.33 / -159.00OK 286.33 / 302.33 / 16.00
18.0OK 286.00 / 302.00 / 16.00OK 286.00 / 302.00 / 16.00
18.3333BAD 285.67 / 127.17 / -158.50OK 285.67 / 301.67 / 16.00
18.6667BAD 285.33 / 243.25 / -42.08OK 285.33 / 301.33 / 16.00
19.0OK 285.00 / 301.00 / 16.00OK 285.00 / 301.00 / 16.00
19.3333BAD 284.67 / 242.75 / -41.92OK 284.67 / 300.67 / 16.00
19.6667BAD 284.33 / 126.83 / -157.50OK 284.33 / 300.33 / 16.00
20.0OK 284.00 / 300.00 / 16.00OK 284.00 / 300.00 / 16.00
20.3333BAD 283.67 / 126.67 / -157.00OK 283.67 / 299.67 / 16.00
20.6667BAD 283.33 / 126.58 / -156.75OK 283.33 / 299.33 / 16.00
21.0OK 283.00 / 299.00 / 16.00OK 283.00 / 299.00 / 16.00

Evidence

Other visible differences beyond compose placement: the system clocks differ; F1 has a one-level battery-edge raster difference; F3 has one-level differences around the search button and tiny glass-edge differences around the tab bar. F6's after capture includes the system “AgentDeviceRunner” return breadcrumb, shifting its status text; the application pixels match. These are disclosed below and retained in the full-screen diffs.

Captured 5/5 requested rows: F1, F2, F3, F4, F6, plus the optional F6 before still. Eight settled stills, eight diff images, and two interaction recordings are public. Each final still passed a pairwise ImageMagick compare -metric AE check with 0 changed pixels, with a 500ms pause between screenshot commands. All final stills, diff masks, and recording frame sheets were visually inspected. The whole screen is retained.

Rendering environment: real Expo development client, driven by AgentDevice 0.21.31 through the Simlock pool (remote daemon 0.21.32). F1/F2/F3/F4: iPhone 15 Pro, iOS 27.2, portrait, 393×852pt, native ratio 3×, PNG 1179×2556. F6: iPad Pro 11-inch (M4), iPadOS 27.2, landscape-left, 1210×834pt, native ratio 2×, PNG 2420×1668, expanded 380pt sidebar. Both devices reported the standard iOS text-size category Large, with fresh app defaults and no text-size changes. Model and OS were checked in system Settings → General → About. Every row uses the same synthetic empty state: no environment connections, Chats selected, keyboard closed, default scroll position. Themes are stated per row.

Before runs ade473018d, native fingerprint b311465e7b6c481996afb625ee4278e391dba856; after runs 0727bded22, native fingerprint 97c1225e9c0dc7fe968ad0ae9314259829ddbe3d. Installer JSON confirmed each fingerprint on both devices. Both clients load the same existing Metro JavaScript from the after worktree. Only F2/F3 change the JS host inset identically on each side; the native clients remain the real before/after builds.

Diff masks use ImageMagick compare -fuzz 0% and -fuzz 5%, with changed pixels amplified to full magenta and unchanged pixels black. Thus a single-level change is visible in Diff0.

F1 — iPhone 15 Pro, portrait, iOS 27.2, Light, stock right inset 12pt. Separate settled idle stills beside the F4 recordings. Both: Settings right edge 292pt, compose x=308pt, size 62×62pt, gap 16pt. Application content and tab-bar pixels match. Clock changes 7:38 → 7:41; the only other difference is a one-level battery-edge raster change.

BeforeAfterDiff0, fuzz 0%DiffT, fuzz 5%
ade473018d0727bded22ade473018d → 0727bded22ade473018d → 0727bded22

F2 — iPhone 15 Pro, portrait, iOS 27.2, Dark, Chats. Harness capture in the real app; compact host right inset 13.6667pt instead of 12. Before: compose covers Settings, x=247pt; Settings right edge 290.333pt. After: compose x=306.333pt, exactly 16pt after Settings, still 62×62pt. Visible differences: compose and its glass circle move 59.333pt right, revealing Settings and the underlying bar; clock 7:39 → 7:43. No header or body pixels change.

BeforeAfterDiff0, fuzz 0%DiffT, fuzz 5%
ade473018d0727bded22ade473018d → 0727bded22ade473018d → 0727bded22

F3 — iPhone 15 Pro, portrait, iOS 27.2, Light, Chats. Harness capture in the real app; compact host right inset 12.3333pt instead of 12. Before: compose covers the PRs/Usage area, x=128.667pt; Settings right edge 291.667pt. After: compose x=307.667pt, exactly 16pt after Settings, still 62×62pt. Visible differences: compose and its glass circle move 179pt right, revealing PRs/Usage; clock 7:40 → 7:43; one-level search-edge and tiny tab-bar glass-edge raster differences. No body pixels change.

BeforeAfterDiff0, fuzz 0%DiffT, fuzz 5%
ade473018d0727bded22ade473018d → 0727bded22ade473018d → 0727bded22

F4 — iPhone 15 Pro, portrait, iOS 27.2, Light, stock insets. Same driver on both clients. Command offsets from recording start are below; each hold lasts 2 seconds and includes release. The recordings show the native pressed lens, followed by settled Chats; compose is absent on PRs/Usage/Settings and returns on Chats. The compose tap opens “Choose project,” then Back returns to Chats. The stock identifier path is not claimed as F5 fallback coverage.

InteractionBefore offsetAfter offset
Chats idle0.00s0.00s
Tap PRs2.00s2.00s
Tap Usage4.29s4.36s
Tap Settings6.66s6.67s
Hold Chats from Settings, then release9.34s9.33s
Hold selected Chats, then release13.77s13.77s
Normal tap Chats18.18s18.01s
Tap compose / New chat20.57s20.30s
Back to Chats23.80s23.45s

GitCafe rejected the MP4 attachment (state=rejected, status nextAction=none). The public recordings below are animated GIF conversions at 393×852, 12fps, approximately 2.18MB before / 2.11MB after, under the 10MiB cap. The original whole-screen MP4s remain in scratch: 27.933s / 4,608,638 bytes before, 27.767s / 4,382,295 bytes after, both encoded by the recorder at 1178×2556. The recorder output is one pixel narrower than the native PNG screenshots. No interaction was trimmed. Visible non-product differences are the clock and small command/transition timing variations.

F4 hold frame — iPhone 15 Pro, portrait, iOS 27.2, Light, stock insets. The same frame 15.8s into each recording (holding the already-selected Chats tab, glass lens visible), extracted from the original whole-screen MP4s. Compose stays 62×62pt, 16pt after Settings, on both sides while the lens is shown. Diff0 is lit by H.264 compression noise because both frames come from lossy recordings; at 5% the only changed pixels are the status-bar clock digits (7:38 → 7:42).

BeforeAfterDiff0, fuzz 0%DiffT, fuzz 5%
ade473018d0727bded22ade473018d → 0727bded22ade473018d → 0727bded22

F6 — iPad Pro 11-inch (M4), landscape-left, iPadOS 27.2, Dark, stock geometry. Both sides show all four sidebar tabs, Chats selected, and no floating compose in the 380pt sidebar footer. The detail pane's existing “New thread” action remains visible. The actual M4 viewport is 1210×834pt, rather than the matrix's illustrative 1194×834pt. Application pixels match: all differences are in the system status bar. The after capture has the “AgentDeviceRunner” return breadcrumb and shifted status text; clock 7:50 before / 7:47 after. Before/after labels identify code revisions; the optional before still was captured later.

BeforeAfterDiff0, fuzz 0%DiffT, fuzz 5%
ade473018d0727bded22ade473018d → 0727bded22ade473018d → 0727bded22

Not exercised: F5, explicitly excluded by the lead's narrowed brief; no identifier-free native harness was captured, so that fallback remains unverified. F7/F8/F9 were omitted as allowed by the matrix. The reported iOS 26.5 case was not exercised; the failing pairs above are explicitly labelled iOS 27.2 inset harnesses.

Not verified:

  • cannot-verify ios-device: the report's physical iPhone 16 Pro (iOS 26, Release build) and the iOS 26.5 simulator runtime were not available; failing geometry was reproduced on iOS 27.2 through the labelled inset harness, not at the stock width on iOS 26.5.
  • The identifier-free fallback path (iOS 26.6, where tab buttons lack the workspace-tab-* identifiers and the lens copy is among the candidates) was not exercised: iOS 27.2 copies the identifiers, so the stock press-and-hold recordings (F4) run the identified path. The lens-copy exclusion there rests on the size difference measured in #28955.
  • Five- and six-item bars (fork-only) were not tested; upstream always shows four tabs.

Driver scripts used for the evidence (scratch, not committed):

Width sweep driver
python
1# Scratch repro driver: steps the compact tab bar host's right inset (JS, via Metro Fast Refresh)
2# and records tab/compose frames from the accessibility tree. Usage: widthsweep.py <session> <outdir> <start> <stop> <steps_per_pt>
3import json, subprocess, sys, time, os
4s, out, start, stop, per = sys.argv[1], sys.argv[2], float(sys.argv[3]), float(sys.argv[4]), int(sys.argv[5])
5f = os.path.expanduser("~/wt/issue-32974/apps/mobile/src/features/layout/workspace/WorkspaceSidebarTabs.ios.tsx")
6orig = open(f).read()
7needle = "(props.compact ? 12 : 0) +"
8assert needle in orig
9def frames(path):
10 d = json.load(open(path)); r = {}
11 def walk(n):
12 if isinstance(n, dict):
13 if n.get("type") == "Button" and n.get("label") in ("Chats","PRs","Usage","Settings","New chat"): r[n["label"]] = n.get("rect")
14 for v in n.values(): walk(v)
15 elif isinstance(n, list):
16 for v in n: walk(v)
17 walk(d); return r
18try:
19 k = 0
20 while start + k / per <= stop + 1e-9:
21 R = round(start + k / per, 4)
22 open(f, "w").write(orig.replace(needle, f"(props.compact ? {R} : 0) +"))
23 time.sleep(4)
24 p = f"{out}/r{R}.json"
25 with open(p, "w") as fh: subprocess.run(["agent-device","snapshot","--json","--session",s], stdout=fh, stderr=subprocess.STDOUT)
26 try: fr = frames(p)
27 except Exception as e: print(R, "snapshot-error", e, flush=True); k += 1; continue
28 last = fr.get("Settings"); c = fr.get("New chat")
29 if not last or not c: print(R, "missing", fr, flush=True); k += 1; continue
30 gap = c["x"] - (last["x"] + last["width"])
31 flag = "OK" if abs(gap - 16) < 2 else "BAD"
32 print(R, flag, "settingsMaxX=%.3f composeX=%.3f gap=%.3f compose=%s" % (last["x"]+last["width"], c["x"], gap, c), flush=True)
33 if flag == "BAD":
34 subprocess.run(["agent-device","screenshot",f"{out}/bad-r{R}.png","--session",s], capture_output=True)
35 k += 1
36finally:
37 open(f, "w").write(orig)
38 print("restored", flush=True)
Capture driver
python
1import datetime
2import json
3import pathlib
4import subprocess
5import sys
6import time
7
8ROOT = pathlib.Path('/tmp/issue-lead-32974-r1/capture')
9RUN = ROOT / datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%S%fZ')
10RUN.mkdir()
11SESSION = 'il32974-cap'
12SOURCE = pathlib.Path('/home/ubuntu/wt/issue-32974/apps/mobile/src/features/layout/workspace/WorkspaceSidebarTabs.ios.tsx')
13
14def command(args, check=True):
15 started = time.time()
16 result = subprocess.run(args, text=True, capture_output=True)
17 with (RUN / 'commands.jsonl').open('a') as log:
18 log.write(json.dumps({'time': started, 'args': args, 'returncode': result.returncode, 'stdout': result.stdout, 'stderr': result.stderr}) + '\n')
19 print('COMMAND', args, 'EXIT', result.returncode, result.stderr, flush=True)
20 if '--json' not in args: print(result.stdout, flush=True)
21 if check and result.returncode:
22 raise RuntimeError(args)
23 return result
24
25def ad(*args, check=True):
26 return command(['agent-device', *map(str, args), '--session', SESSION], check)
27
28def frames(name):
29 raw = ad('snapshot', '--json').stdout
30 (RUN / (name + '.json')).write_text(raw)
31 d = json.loads(raw)
32 found = {}
33 def walk(x):
34 if isinstance(x, dict):
35 if x.get('type') == 'Button' and x.get('label') in ('Chats', 'PRs', 'Usage', 'Settings', 'New chat'):
36 found[x['label']] = x.get('rect')
37 for v in x.values(): walk(v)
38 elif isinstance(x, list):
39 for v in x: walk(v)
40 walk(d)
41 (RUN / (name + '-frames.json')).write_text(json.dumps(found, indent=2))
42 print('FRAMES', found, flush=True)
43 return found
44
45def still(name):
46 time.sleep(2)
47 for attempt in range(6):
48 a = RUN / f'{name}-settle-{attempt}-a.png'
49 b = RUN / f'{name}-settle-{attempt}-b.png'
50 ad('screenshot', a, '--pixel-density', (sys.argv[3] if len(sys.argv) > 3 else 3))
51 time.sleep(.5)
52 ad('screenshot', b, '--pixel-density', (sys.argv[3] if len(sys.argv) > 3 else 3))
53 r = command(['compare', '-metric', 'AE', str(a), str(b), 'null:'], check=False)
54 if r.returncode == 0:
55 final = RUN / f'{name}.png'
56 final.write_bytes(b.read_bytes())
57 print('STILL', final, flush=True)
58 frames(name)
59 return
60 raise RuntimeError('No pixel-identical settled pair')
61
62def inset(value):
63 import re
64 text = SOURCE.read_text()
65 updated, count = re.subn(r'\(props.compact \? (?:12|12\.3333|13\.6667) : 0\) \+', f'(props.compact ? {value} : 0) +', text)
66 assert count == 1, count
67 SOURCE.write_text(updated)
68 time.sleep(5)
69
70def recording(name):
71 ad('record', 'start', RUN / f'{name}-raw.mp4', '--fps', 30)
72 t0 = time.time()
73 actions = []
74 def step(label, *args):
75 actions.append({'seconds': round(time.time() - t0, 2), 'step': label})
76 ad(*args)
77 time.sleep(1)
78 try:
79 time.sleep(2)
80 step('Tap PRs', 'press', 'id="workspace-tab-pullRequests"', '--settle')
81 step('Tap Usage', 'press', 'id="workspace-tab-usage"', '--settle')
82 step('Tap Settings', 'press', 'id="workspace-tab-settings"', '--settle')
83 step('Hold Chats from Settings for 2 seconds, then release', 'longpress', 'id="workspace-tab-threads"', 2000, '--settle')
84 step('Hold selected Chats for 2 seconds, then release', 'longpress', 'id="workspace-tab-threads"', 2000, '--settle')
85 step('Normal tap Chats', 'press', 'id="workspace-tab-threads"', '--settle')
86 step('Tap New chat', 'press', 'label="New chat"', '--settle')
87 frames(name + '-compose-destination')
88 step('Back to Chats', 'press', 'label="Back"', '--settle')
89 frames(name + '-returned')
90 finally:
91 ad('record', 'stop')
92 (RUN / (name + '-steps.json')).write_text(json.dumps(actions, indent=2))
93 print('RECORDING', RUN, flush=True)
94
95if __name__ == '__main__':
96 print('RUN', RUN, flush=True)
97 mode = sys.argv[1]
98 if mode == 'still': still(sys.argv[2])
99 elif mode == 'inset': inset(sys.argv[2]); frames('inset-' + sys.argv[2])
100 elif mode == 'record': recording(sys.argv[2])
101 elif mode == 'open':
102 ad('open', 'com.t3tools.t3code.dev', 't3code-dev://expo-development-client/?url=http%3A%2F%2F<metro-host>%3A8082&disableAutoLaunch=1&disableFab=1')
103 ad('alert', 'accept', check=False)
104 ad('wait', 'text', 'Add environment', 60000)
105 frames('opened')

AI-assisted; verified with the tests and tools listed above.

Checks

25 successful · 14 skipped
All checks passed
0727bde

Added 1 commit

  • fix(mobile): compose button no longer covers the last sidebar tab0727bde
Synced from GitHub

Approvability

Verdict: Approved at 0727bde

Macroscope's review found this PR approvable — The change tolerates sub-point rounding when positioning the compose button beside workspace tabs, preventing it from covering the last tab. Its effects are limited to UI layout, supported by reported before/after simulator checks.

You can add or adjust custom eligibility rules. Learn more.

Synced from GitHub

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

The tab-bar layout now accepts tab-button bounds when the converted width and height each differ from local bounds by less than one point. The filter continues to exclude the enlarged glass-lens copy.

Changes: Tab bar layout

Layer / File(s)Summary
Tab button size filter
apps/mobile/modules/t3-native-controls/ios/T3SidebarTabBarView.swift
WorkspaceTabBar.layoutSubviews() now accepts differences below one point for both width and height when filtering tab-button bounds.

Estimated code review effort: 2 (Simple) | ~5 minutes

Change: Bug fix · Severity of issue fixed: Low

Possibly related PRs

Suggested reviewers: jerematix, macroscopeapp

Merge Risk: External image Minimal · up to 0727b

No actionable merge-blocking issue is established for the tab-button sizing change; it is ready for normal checks.

Pre-merge checks: Passed 4

Check nameStatusExplanation
Title checkPassedThe title names the user-visible fix: the compose button no longer covers the last tab on iPhone. The raw summary and objectives show the change is a tolerance-based size filter in WorkspaceTabBar.lay…
Description checkPassedThe description follows the template sections: Problem, Change, Scope and approval, and Verification. It states the problem with reproduction steps and environment, and it explains the tolerance-based…
Linked Issues checkPassed[#32974] requires the iOS tab-bar sizing filter to accept real tab buttons that fail exact size equality, while still excluding the glass lens's magnified copy. The diff at head (base ade473018d to he…
Out of Scope Changes checkPassedThe whole-PR diff touches one file, apps/mobile/modules/t3-native-controls/ios/T3SidebarTabBarView.swift, with +5/-1 lines. The only changed code is the filter predicate inside `WorkspaceTabBar.layo…
Full details: Title check

Explanation

The title names the user-visible fix: the compose button no longer covers the last tab on iPhone. The raw summary and objectives show the change is a tolerance-based size filter in WorkspaceTabBar.layoutSubviews that restores tab-button placement for compose. The title states the outcome, not the mechanism, which is acceptable. It is in conventional commit form and fits the repository's example style.

Full details: Description check

Explanation

The description follows the template sections: Problem, Change, Scope and approval, and Verification. It states the problem with reproduction steps and environment, and it explains the tolerance-based change. It links the issue (#32974) and gives a detailed verification plan with a width sweep, before/after screenshots, and a recording. It discloses what was not verified. The Scope section cites the linked issue and says the tolerance is the one the issue proposed. It does not include explicit maintainer approval, but the issue link covers the template requirement. The Verification section is long, and the 'Evidence' prose is dense, but the required content is present. The description is mostly complete.

Full details: Linked Issues check

Explanation

[#32974] requires the iOS tab-bar sizing filter to accept real tab buttons that fail exact size equality, while still excluding the glass lens's magnified copy. The diff at head (base ade473018d to head 0727bded22) replaces convert($0.bounds, from: $0).size == $0.bounds.size in WorkspaceTabBar.layoutSubviews with abs(size.width - $0.bounds.width) &lt; 1 &amp;&amp; abs(size.height - $0.bounds.height) &lt; 1. This is the tolerance the issue proposes. The row union then uses only the buttons that pass, so compose is placed after the last real tab. The issue names no other coding requirement. The issue's non-coding content (reproduction steps, environment notes) does not add requirements. The PR states that Swift compilation was checked only through an EAS build, and no Swift tests were added. The issue does not ask for tests, so this is not a failure. Unverified items are the iOS 26 identifier-free fallback path, the iOS 26.5 and physical iPhone 16 Pro cases, and fork-only five- and six-item bars. The issue's own requirement is met by the code change. The PR's width sweep on iOS 27.2 supports the fix. It does not cover those unverified runtimes.

Full details: Out of Scope Changes check

Explanation

The whole-PR diff touches one file, apps/mobile/modules/t3-native-controls/ios/T3SidebarTabBarView.swift, with +5/-1 lines. The only changed code is the filter predicate inside WorkspaceTabBar.layoutSubviews, plus a comment that explains the tolerance. The change connects directly to [#32974]. No other behavior, public declarations, or unrelated files change. The large amount of screenshot and driver-script evidence in the PR description is supporting material, not repository change, and it does not add out-of-scope code.

Finishing Touches

Generate docstrings

  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)

  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
External image Recent review info

External image Run configuration

External image Commits

Reviewing files that changed from the base of the PR and between ade473018d32003658b7d38693e083ee2e314bd8 and 0727bded228e92fc3eb2bbca0831abddb2134d45.

External image Files selected for processing: 1

  • apps/mobile/modules/t3-native-controls/ios/T3SidebarTabBarView.swift

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

[GPT-6.1 Sol][Codex] on behalf of shivam.

Handoff from https://github.com/pingdotgg/t3code/pull/32984, head 9e545dd4b80e6192c2d8c78a2b8f15489148b48b, based on ade473018d32003658b7d38693e083ee2e314bd8.

Source investigation found the exact converted-size comparison can exclude real tab buttons after rounding, shortening the row used to place compose. Our candidate uses the same symmetric tolerance below 1pt on width and height. Independent Codex source review found no actionable code issues.

No device reproduction or native build was completed by this job. Simlock exposed no iOS devices and shiv's T3 was unreachable. There are no before/after captures or interaction recordings from us; layout and glass-lens long-press behavior stayed unverified.

Checks for our head: pre-commit format/lint and git diff --check passed. Boat job yerqfyygu2tqemva on Linux ran vp run check:changed origin/main: format and typecheck of two packages passed, exit 0. CI had no failures. Native static analysis linted the changed Swift file with zero violations; it did not compile or run the iOS app. These results cover our head, not this PR's head.

Stopping this job and its scheduled device retry now that Julius has taken over.