UTI: top nav bar with text-driven visibility#9159
Conversation
Cover the pieces extracted for testability: the contentTopInset pure function (per-mode content offset), the nav bar layout params and widget top offset in NativeInputLayoutCoordinator, and the bindInputModeNavBar click wiring including null-safety when buttons are absent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
shouldShowNavBar encodes the browser-context + empty-field rule; shouldAnimateNavBar guards idempotent visibility toggles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
translationY slide with a dedicated animator so a visibility toggle never cancels the widget morph. Top mode rides the widget with the bar; bottom mode animates the bar alone. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
bindInputEvents gains an onInputTextEmptyChanged callback fired on every text change from the existing length-based emptiness check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Attach the nav bar only for browser input, then show it while the field is empty and slide it away once there is text; top mode rides the widget with the bar. Duck.ai no longer shows the bar. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
configureContentOffset registered a fresh set of layout listeners on every call, closing over that call's navBarInsetPx and never removed except on widget detach. The manager called it twice at attach (inset 0, then inset H) plus once per empty/text toggle, so in bottom mode the leftover listener sets fought each other every frame via requestLayout, causing a silent sustained layout loop; in top mode the sets simply accumulated per session. Listeners now register once, and the live inset lives in a mutable field they read directly. updateNavBarInset updates the field and replays the stored re-apply hook instead of re-registering. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The toggle-row back arrow now mirrors the inverse of the top nav bar: hidden while the field is empty (the nav bar shows its own back then) and shown once there is text, so the two are never visible together. Browser only; the arrow never renders in Duck.ai / contextual. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Route the nav bar back through the widget's onBackPressed so it fires the same back-button pixel as the toggle-row back (the two are complementary, so otherwise empty-state back presses go uncounted). Give the nav bar's fire/tabs/menu views distinct inputModeWidgetNav* ids instead of reusing the widget's, so the two views attached to the same parent no longer share ids (view-state / a11y hazard). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In top-omnibar mode the nav bar hides by riding the widget up via translationY, a render transform that leaves the widget's layout bottom unchanged. The autocomplete offset read that layout bottom, so the suggestions kept padding for the widget's old nav-bar-shown position, leaving a gap the size of the nav bar between the widget and the first result. Compute the top offset from the widget's on-screen bottom (layout bottom plus translationY) and reapply it when the nav bar toggles, since a translationY change fires no layout listener. The bottom offset stays layout-based because the bottom-mode nav bar does not ride the widget. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
applyForcedBottomTranslation runs a frame after attach and reset a top-anchored widget's translationY to zero. When reopening the unified input with prefilled text in top-omnibar mode, the nav bar is hidden and the widget rides up by the nav bar height via translationY; the reset undid that ride, dropping the widget and leaving a gap under the top chrome with misaligned content padding. Only reset bottom-anchored widgets. A top-anchored widget's translationY is owned by the nav bar visibility, so leave it alone. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With a bottom omnibar the autocomplete suggestions list spans the top of the screen and drew over the nav bar strip, hiding it whenever the Duck.ai tab was selected (its suggestions show even with an empty field). Two changes fix it: - Offset the suggestions list below the nav bar in bottom mode by the nav bar inset (was hard-zero), reusing the updateNavBarInset reapply hook so it tracks the empty/text toggle. - Float the nav bar above the suggestions list via translationZ, but only with a bottom omnibar. A top omnibar has no such overlap (the legacy AppBar already occupies the top), and the elevation shadow there would tint the bar so its background no longer matches the content. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a nativeInputNavBar sub-feature to DuckChatFeature (default internal) as an extra guardrail on the input-mode nav bar. Expose it through the DuckChat API as observeNativeInputNavBarEnabled and have the manager gate nav bar creation on it. The bar is now created only when the flag is enabled AND the address bar is in Search & Duck.ai mode. Search-only users, and users without the flag, never get it. Duck.ai conversation and contextual input are unchanged (still no bar). Decision extracted to the pure shouldCreateNavBar for unit testing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 6a4123e. Configure here.
For users with the nativeInputNavBar flag enabled and the address bar in Search & Duck.ai mode, the tabs button lives in the input-mode nav bar. Hide the duplicate tabs button in the return hatch for them by folding observeNativeInputNavBarEnabled into the hatch's shouldShowTabsButton. Search-only, flag-off, and Duck.ai-conversation cases are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@malmstein From testing, when I switch from Duck.Ai and Search to Search only, the Native input bar is still visible until I restart the app, Not the case for the other way around. |
The nav bar was decided only at showNativeInput. If the address bar switched from Search & Duck.ai to Search-only (or the flag flipped off) while the input stayed open, the already-attached bar kept showing until the widget was torn down — a restart in practice. Fold the create-time gate into the runtime visibility decision (navBarShouldBeVisible = shouldCreateNavBar && shouldShowNavBar) and re-apply it from the inputModeCapability and nav-bar-flag observers, so a live switch to Search-only now hides the bar. Creation still happens at show time, so the reverse (becoming Search & Duck.ai mid-session) is picked up on the next open, as before. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Changing the address bar from Search & Duck.ai to Search-only while the unified input was open left the native widget on screen as a toggle-less variant instead of reverting to the legacy omnibar; it only corrected on the next close/refocus. The browser stops using native input in search-only (isNativeInputActive() is false), so when the input mode changes and the input is no longer active, hide the widget (restoring the legacy omnibar) rather than only re-evaluating the nav bar. hideNativeInput no-ops when nothing is shown, and Duck.ai (still active) is unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| private var navBarShown: Boolean? = null | ||
| private var navBarHeightPx: Int = 0 | ||
| private var navBarIsBottom: Boolean = false | ||
| private var navBarTabCountLiveData: LiveData<Int>? = null |
There was a problem hiding this comment.
the tabs source is already handed to us as LiveData<List> by the fragment, and the widget's own bindTabCount takes LiveData too — this just does tabs.map { it.size } and mirrors that same binding so both tab counts update the same way. didn't want to convert this one button to Flow and diverge from the sibling. happy to migrate both together if we're moving that way.
There was a problem hiding this comment.
I’ll add this to tech debt cleanup in https://app.asana.com/1/137249556945/project/1211654189969294/task/1216509599440244?focus=true
|
Thanks @karlenDimla ! |

Task/Issue URL: https://app.asana.com/1/137249556945/project/1174433894299346/task/1216390319388562?focus=true
Tech Design URL (if applicable):
Description
Adds a persistent top nav bar to the Unified Text Input (UTI) and wires the complementary visibility behaviours so the chrome around the input feels consistent.
Feature flag / guardrail
nativeInputNavBarsub-feature onDuckChatFeature(default internal), exposed through theDuckChatAPI asobserveNativeInputNavBarEnabled().Nav bar visibility
InputContext.BROWSER) and only while the input field is empty. Whitespace counts as text.Toggle-row back arrow
NTP return hatch
Notable internals
configureContentOffsetregistered its layout listeners additively; recomputing the offset per toggle would have stacked competing listeners and silently looped layout in bottom mode. Listeners now register once, with a cheapupdateNavBarInset(px)for subsequent changes.ValueAnimator(independent of the widget↔omnibar morph) with a cancellation guard.onBackPressed()so it fires the same back-button pixel as the toggle-row back (consistent telemetry).inputModeWidgetNav*ids so the two views attached to the same parent don't share ids.Steps to test this PR
Enable the feature
nativeInputNavBarflag is enabled (internal builds default to on).Browser, top omnibar
Browser, bottom omnibar
Search Only / flag off
nativeInputNavBarflag) → no nav bar in any state.Duck.ai conversation / contextual
Return hatch
UI changes
Note
Medium Risk
Touches browser chrome layout, animations, and NTP/hatch visibility with many edge cases (top vs bottom omnibar, prefilled text, mode/flag toggles); risk is UI regressions rather than security, mitigated by feature flag and extensive unit tests.
Overview
Adds a persistent top nav bar (back, fire, tabs, menu) to unified native browser input, behind the new
nativeInputNavBarflag and only in Search & Duck.ai mode—not in search-only, Duck.ai chat, or contextual input.The bar is visible only while the input field is empty; typing slides it away (in top omnibar mode the widget rides up with it). Clearing text brings it back. The toggle-row back arrow is now the inverse: hidden when empty (nav bar back shows) and shown when there is text, so only one back control appears.
BrowserTabFragmentwires tab switcher and browser menu into native input callbacks.NativeInputManagercreates/binds the nav bar, observes the flag and input-mode changes (tearing down the widget on search-only), and drives visibility viaanimateNavBarVisibility.NativeInputLayoutCoordinatoroffsets widget, NTP/browser content, and autocomplete using nav-bar inset and translation-aware widget bounds;updateNavBarInsetavoids stacking layout listeners.The NTP return hatch hides its tabs button when the nav bar flag is on, avoiding duplicate tabs affordances.
DuckChatexposesobserveNativeInputNavBarEnabled(). Unit tests cover visibility rules, offsets, animator snap behavior, and nav bar wiring.Reviewed by Cursor Bugbot for commit 900b9eb. Bugbot is set up for automated code reviews on this repo. Configure here.