Skip to content

fix(web): keep the Workspace rail clear of the OS status and nav bars - #3587

Open
btli wants to merge 20 commits into
omnigent-ai:mainfrom
btli:fix/android-right-rail
Open

fix(web): keep the Workspace rail clear of the OS status and nav bars#3587
btli wants to merge 20 commits into
omnigent-ai:mainfrom
btli:fix/android-right-rail

Conversation

@btli

@btli btli commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Related issue

Closes #3585

Summary

On a tablet or unfolded foldable (Pixel 10 Pro Fold), the right Workspace rail rendered underneath the OS status bar — its Files / Agents / Shells / Tasks tab icons sat in the occluded band and could not be tapped — and its bottom edge ran under the gesture-nav bar.

The rail is md:m-2 (8px on every edge) and, being hidden md:flex, only exists at ≥768px. Every native-shell inset rule lives inside @media (width < 48rem) (index.css:522731), so none of them can reach it; it is also absent from the shared panel selector lists on both the CSS side and the Android-injected side. Its top edge therefore landed at y = 16px, inside a 24–48dp status bar.

  • Add the safe-area margins outside the width gate, scoped to :is([data-ios-native], [data-android-native]), so both native shells get it and browser/Electron are untouched.
  • Mirror the identical declaration into the Android injected sheet (ensureInsetStyles) so shells pointed at a server running an older web build get the fix from an APK update. Same property, same value, so the two stylesheets are idempotent and cannot double-count.
  • Separate, independently droppable commit: the execution-logs panel docks as a full-height rail at md+ but was missing from both selector lists, so it had no safe-area padding at any width. Debug-mode only, which is why it went unnoticed.

Uses --omnigent-safe-*, not --omnigent-inset-* — the latter folds in the native bottom-bar footprint and would double-count against the bottom edge.

BEFORE                          AFTER
┌──── status bar ────┐          ┌──── status bar ────┐
│ ▣ ▣ ▣ ▣  ← dead    │          ├────────────────────┤
├────────────────────┤          │ ▣ ▣ ▣ ▣  ← tappable│
│  workspace rail    │          │  workspace rail    │

ELI5: the rail is a floating card pinned 8px from the screen edges. On phones a separate set of rules pushes UI clear of the status bar, but those rules switch off at tablet width — exactly where this card starts existing. So it kept its 8px and slid under the clock and battery icons.

Test Plan

  • npx vitest run src/index.css.test.ts16 passed.
  • The regression test pins the property that actually matters: the rule must apply at md+. It asserts the rule has no enclosing at-rule of any kind, that every rule setting the rail's margins is top-level and uses --omnigent-safe-* rather than the inset vars, and that exactly one such rule exists.
  • Mutation-checked throughout — an early version of this test passed while the fix was inert, so each hardening was accepted only after the corresponding mutation went red. The suite now fails for: wrapping the rule in @media (width < 48rem), @media (max-width: 767px) or @supports; appending a later @media (min-width: 48rem) block that zeroes the margins; a second top-level rule reintroducing the inset vars; shorthand margin and logical margin-block* overrides; and the same selector written single-quoted, unquoted or with extra spacing.
  • pre-commit run --files <changed files> — passed. npm run lint — no new findings.
  • Reviewed by three independent engines (gpt-5.6-sol, Grok 4.5, Gemini 3.1 Pro) over six rounds; all three report clean on the final state.

Note the uniqueness assertion will deliberately fail if someone later adds a second margin rule for the rail. That is intended: this rule was silently inert on every tablet until now, so a future change to it should require a human to look rather than pass quietly.

Demo

Recorded on an Android emulator at 800×1280 @ 160dpi (an 800dp-wide viewport, i.e. the md+ tablet/unfolded-foldable layout) with the tall display-cutout overlay enabled, connected to a local server.

Before (main APK) After (this PR's APK)
before — rail tab icons render inside the status bar; taps do nothing after — rail starts below the status bar; the same taps switch tabs

Before: the Workspace rail's Files / Agents / Shells tab icons render inside the status bar, sharing the row with the clock and battery; tapping them does nothing (the taps land on the system bar), while content lower in the rail still responds. After: the rail's card starts below the status bar and clear of the gesture-nav area, and the same taps switch tabs normally. Full-resolution MP4s alongside the GIFs on the asset branch.

The floating server pill visible near the top of the "after" capture still overlaps web chrome — that is the separate defect fixed by #3589 and is untouched by this PR.

Type of change

  • Bug fix
  • Feature
  • UI / frontend change
  • Refactor / chore
  • Docs
  • Test / CI
  • Breaking change

Test coverage

  • Unit tests added / updated
  • Integration tests added / updated
  • E2E tests added / updated
  • Manual verification completed
  • Existing tests cover this change
  • Not applicable

Coverage notes

The CSS contract is covered by the mutation-checked regression test above, and the rendered result is now verified on an emulator at md+ width (see Demo), including the tap behaviour before and after. Not yet verified on physical foldable hardware. The inset values themselves are unchanged and already exercised on phones — this change only adds a new consumer of them.

Deliberately out of scope, and worth a follow-up: the same @media (width < 48rem) gate means that at iPad widths the chat header, conversation content, terminal view and composer are also uncovered, because iOS has no injected-sheet backstop the way Android does. This PR fixes the rail on both shells but does not make iPad correct in general.

Changelog

The workspace rail on Android tablets and foldables no longer slides under the status bar, so its tab icons stay tappable

@github-actions github-actions Bot added the size/M Pull request size: M label Jul 30, 2026
@btli
btli force-pushed the fix/android-right-rail branch from 2033967 to 57c8698 Compare July 30, 2026 18:39
@btli
btli marked this pull request as ready for review August 4, 2026 09:27
@github-actions
github-actions Bot requested a review from serena-ruan August 4, 2026 09:28
@btli

btli commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Withdrawing — this isn't the right fix for the rail inset issue; a different approach is coming.

@btli btli closed this Aug 4, 2026
@btli

btli commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Reopening after discussion — this approach is correct after all.

@btli

btli commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Pushed 2 commits:

  • The workspace rail pads content clear of the OS bars on all four edges instead of shrinking with top/bottom margins, so its background and divider stay flush; lateral insets fold into --omnigent-safe-left/right.
  • Published insets include the display cutout, which systemBars() alone omits; the injected fallback sheet mirrors the same rules.
  • Plus a test-helper dedup in the CSS contract tests.

Verification: web suite green on this branch (4,715); Android green except one pre-existing failure (configuration change updates system bar icon polarity) — this branch's base reads resources.configuration, which lags direct onConfigurationChanged calls under Robolectric. #3800 fixes that by passing the Configuration in, hence the rebase note below.


Recommended merge order — this PR is one of seven fixed and verified together; the fully integrated reference (all cross-PR conflicts resolved, 5,003 web tests + 273/273 Android tests green) is btli:test-android-ui-integration @ 3fa45ff3.

Android track (suggest landing first — security fixes, and the later two resolve against it):

  1. fix(android): sign in to servers behind front-door auth proxies (Databricks Apps) #3800 — no dependencies
  2. fix(web): keep the Workspace rail clear of the OS status and nav bars #3587 — rebase after fix(android): sign in to servers behind front-door auth proxies (Databricks Apps) #3800: it picks up applySystemBarContrast(Configuration), which fixes this branch's one pre-existing Robolectric failure; and since main now has flush rails (feat(web): make the rails flush boxes and move the canvas gradient #4020), the rebase should also drop the stale +16 from --workspace-panel-offset (see integration commit 8b687d7c)
  3. fix(android): keep the server switcher centred over the chat column, clear of header controls #3589 — after fix(android): sign in to servers behind front-door auth proxies (Databricks Apps) #3800: both add constructor callbacks to OmnigentBridgeListener; union them (resolution in integration merge 3fa45ff3)

Web track (file-disjoint from the Android track; order within it matters):
4. #3985 — base swipe-actions feature
5. #4060 — folder context menu
6. #4057 — mobile ungroup drop zone
7. #4065 — hold: superseded by the unified row-gesture recognizer, which builds on #3985 and interacts with #4057/#4060; after 4–6 land, update it to the recognizer commits (69a8ada5, bf597b87, e388aacf, reachable on the integration branch)

btli added 8 commits August 6, 2026 10:44
On a tablet or unfolded foldable the right Workspace rail rendered under
the status bar, leaving its tab icons un-tappable, and ran under the
gesture-nav bar at the bottom.

The rail is `md:m-2` and only renders at md+, but every native-shell
inset rule lives inside `@media (width < 48rem)` — so none of them reach
it — and it is absent from the shared panel selector lists on both the
CSS and Android-injected sides. Add the safe-area margins outside the
width gate, and mirror the same declaration into the Android injected
sheet so shells pointed at an older web build get the fix too.

Uses --omnigent-safe-*, not --omnigent-inset-*: the latter folds in the
native bottom-bar footprint and would double-count.

Signed-off-by: Bryan Li <bryan.li@gmail.com>
The panel docks as a full-height rail at md+ but was missing from both
inset selector lists, so it had no safe-area padding at any width. It is
reachable only in debug mode, which is why it went unnoticed.

Pre-existing gap, adjacent to the Workspace rail fix rather than part of
it.

Signed-off-by: Bryan Li <bryan.li@gmail.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>
A media-gated override appearing later in index.css with the same
selector previously slipped past the ancestry check, which only
inspected the first matching rule. Assert over every rule that sets
the Workspace rail's margins so a later override at md+ fails the
suite instead of silently winning the cascade.

Signed-off-by: Bryan Li <bryan.li@gmail.com>
The safe-area assertion read only the first matching rule, so a second
top-level rule could reintroduce the composite inset vars — double
counting the native bottom bar — while the suite stayed green.

Signed-off-by: Bryan Li <bryan.li@gmail.com>
Only the longhand properties were matched, so a shorthand `margin`
declaration set both edges without ever being checked for an enclosing
at-rule.

Signed-off-by: Bryan Li <bryan.li@gmail.com>
Matching the exact selector text let an equivalently-spelled one through,
and checking only the first rule let a later override zero the margins
without naming a banned variable. Match on the rail's aria-label and
require the margin rule to be unique.

Signed-off-by: Bryan Li <bryan.li@gmail.com>
margin-block and its start/end forms set the same edges as the rule
being guarded, so an override written that way was never matched.

Signed-off-by: Bryan Li <bryan.li@gmail.com>
btli added 10 commits August 6, 2026 10:44
The rule scan keyed on one literal spelling, so the same selector written
with single quotes, no quotes, or extra spacing introduced a margin the
uniqueness and safe-area checks never saw.

Signed-off-by: Bryan Li <bryan.li@gmail.com>
The scan matched any spelling of the rail's attribute selector, but the
ancestry check then re-found one exact literal and skipped the rule when
it differed — two sources of truth for the same question, and a rule
whose nesting was never actually checked.

Signed-off-by: Bryan Li <bryan.li@gmail.com>
…tors

The ancestry walk built named at-rule records that no assertion ever
read — every use compared them to an empty list. Brace depth answers the
same question in a third of the lines and is stricter: it also rejects
the rule being nested inside a plain rule, which the ancestor form let
through.

Also stops `scroll-margin-top` and friends registering as margin
overrides.

Signed-off-by: Bryan Li <bryan.li@gmail.com>
The rail now pads content clear of the OS bars on all four edges
instead of shrinking with top/bottom margins, so its background and
divider stay flush. Lateral insets are folded into --omnigent-safe-left
and -right and the published insets include the display cutout, which
systemBars() alone omits. The injected fallback sheet mirrors the same
rules.

Co-authored-by: Isaac

Signed-off-by: Bryan Li <bryan.li@gmail.com>
The Workspace-rail and full-height-panel checks each spelled out the same
four padding expectations and the same stylesheet append/remove dance, and
three call sites repeated the selector-extraction slice. They now go
through withStyle, expectSafeAreaPadding, and selectorOf. Also records why
the injected Android inset CSS must avoid backticks and `${`.

Co-authored-by: Isaac

Signed-off-by: Bryan Li <bryan.li@gmail.com>
The injected fallback sheet flattened index.css's media split, so at md+
widths panels docked inside the already-padded Workspace rail (frameless
FileViewer, right-docked drawers) were safe-area padded twice. Mirror the
(width < 48rem) guard around the :is(...) panel rule and pin it with a
structural test.

Also decouple the CSS layout test from WorkspacePanel's prop surface: a
stub aside carries the selector contract, a source assertion pins the
component's aria-label, and brace-depth scanning shrinks to a blanked-
source count.

Co-authored-by: Isaac
Signed-off-by: Bryan Li <bryan.li@gmail.com>
A local uv invocation rewrote the lock's package URLs to an internal
proxy; restore the normalized PyPI form the lock check expects.

Co-authored-by: Isaac
Signed-off-by: Bryan Li <bryan.li@gmail.com>
The rail only exists at md+, outside the mobile drawer rules, so its
safe-area padding has its own rule; this pins the injected-bridge ->
data-android-native -> inset-fold -> computed-padding chain end to end
with distinct per-edge insets.

Co-authored-by: Isaac
Signed-off-by: Bryan Li <bryan.li@gmail.com>
Execution-logs / file-viewer / terminals panels replace the Workspace
rail in the same right-edge slot at md+, so they slid under the status
bar and cutout the rail was just fixed to avoid. Pad their top, bottom
and right edges (content sits to the left), exempting panels nested
inside the already-padded rail, in both index.css and the injected
fallback sheet. Share the CSS block-extraction across the test file.

Co-authored-by: Isaac
Signed-off-by: Bryan Li <bryan.li@gmail.com>
The injected sheet is interpolated into a JS template literal, so a
backtick or dollar-brace in the CSS breaks the script at runtime while
the style-tag tests keep passing.

Co-authored-by: Isaac
Signed-off-by: Bryan Li <bryan.li@gmail.com>
@btli
btli force-pushed the fix/android-right-rail branch from 906a6f1 to 2455241 Compare August 6, 2026 18:45
@btli

btli commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

This PR is part of a stacked train rebased onto current main so the whole set merges conflict-free in order. Full order: #3985#4065#4060#4057 (web/Sidebar train) and #3587#3589#3800 (android train). The two trains are independent of each other.

This PR is 1st in the android train (based directly on main) — mergeable now. Until its predecessors merge, this PR's diff temporarily includes their commits; it collapses to a clean, self-contained diff automatically as each predecessor lands.

btli added 2 commits August 6, 2026 12:05
The chat header's right edge clears the inline workspace rail via
--workspace-panel-offset. The +16 covered the rail's old outer margins;
the rail is flush now, so the offset is the rail width itself.

Co-authored-by: Isaac
Signed-off-by: Bryan Li <bryan.li@gmail.com>
…bars

Scoping the injected panel inset rule to phone widths also stripped the
conversations sidebar's insets at md+, where it is a pinned column at the
left screen edge — unfolded, its header rendered under the status bar.
Pad its exposed edges (top/bottom/left) at md+ in index.css and mirror
the rule in the injected fallback sheet. Android-only: iOS md+ keeps its
current layout.

Signed-off-by: Bryan Li <bryan.li@gmail.com>
@btli
btli force-pushed the fix/android-right-rail branch from bdb57cc to a2cb59a Compare August 6, 2026 19:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P1-high Priority: major feature broken, no workaround size/XL Pull request size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Android/iPad: right Workspace rail renders under the status bar at tablet widths — tab icons un-tappable

2 participants