feat(omnibox): Phase 5 — Stability, Polish & Flicker-Free Preloading#226
feat(omnibox): Phase 5 — Stability, Polish & Flicker-Free Preloading#226iamEvanYT wants to merge 14 commits intoevan/omnibox-refactor/mainfrom
Conversation
- Add omnibox_shortcuts + bookmarks table schemas and migration - Implement OmniboxShortcutsService with recordUsage, search, cleanup - Wire IPC handlers, FlowOmniboxShortcutsAPI interface, preload bridge - Build ShortcutsProvider (async, max 3, range 1000-1450, 7-day decay) - Record shortcut on omnibox selection in Omnibox.openMatch() - Add BookmarkProvider stub with TODOs (pending bookmarks system) - Add bookmarks data-provider stub (searchBookmarks, isUrlBookmarked) - Enhance dedup with cross-provider merge (bookmark+history, inline) - Update UI with bookmark/shortcut icons, labels, and descriptions
…ainst current input
The SQL LIKE pattern was checking if the stored inputText started with
the current input (inputText LIKE 'gith%'), when it should check if the
current input starts with the stored inputText ('gith' LIKE inputText || '%').
This fixes core shortcut matching: typing 'gith' now correctly matches
shortcuts stored for 'gi', 'git', 'gith' rather than 'gith', 'github'.
Implement scoreMatchQuality() and computeRelevance() per design doc sections 6.3 and 6.4. Provides base relevance ranges per match type, frecency normalization, host/path/title match quality scoring, input length weighting, and bonus/penalty adjustments.
…der timing diagnostics Rewrite autocomplete controller with: - Default match tracking with shouldUpdateDefault() logic (design doc §9) - Short input caution: 1-2 char inputs require >1300 relevance for non-verbatim default - Arrow-key navigation lock: onUserArrowKey() suppresses result updates, onUserKeystroke() clears lock and applies buffered matches - Provider timing diagnostics with ProviderTiming interface for debug page
Add trailing slash normalization on all paths, default port removal (80/443), percent-encoding normalization (decode unreserved, uppercase hex digits), fragment stripping for dedup, and stable query parameter sorting per design doc §8.
Serialize IMUI index to localStorage on populate with version stamp, restore from cache on next open (1-hour max age), then refresh in background. Add diagnostic getters: wordCount, prefixCount, lastRefresh per design doc §4.5.
…ssing Fix omnibox flickering by eliminating loadInterface() calls on show. The omnibox renderer is now loaded once and stays alive, receiving show/hide params via IPC messages instead of URL reloads. - Add OmniboxShowParams type and onShow/onHide to FlowOmniboxAPI - Add omnibox:do-show and omnibox:do-hide IPC channels in preload - Main process Omnibox class: track initialLoadComplete, add sendShowEvent() and sendHideEvent(), hide() notifies renderer - Update all 4 entry points (IPC handler, new-tab, command palette, address bar) to use sendShowEvent() instead of loadInterface()
Convert OmniboxMain from reload-on-show to a persistent component: - Listen for flow.omnibox.onShow/onHide IPC events to control visibility - Reset state (input, matches, selection) on each show event - Use openInRef instead of URL params for current/new_tab mode - Wire up arrow key events to controller.onUserArrowKey() - Route keystroke input through controller.onUserKeystroke() - Expose diagnostic getters (providerTimings, IMUI stats) on Omnibox class - Maintain backward compat with URL params for initial load
Expand debug page from 2 to 4 tabs: - Scoring tab: displays all ScoringSignals fields and dedup key - Timings tab: provider name, duration, match count per provider - Debug tab: add IMUI state panel (word count, prefix count, last refresh) - Fix inline completion display to use template literal
Build artifacts for all platforms are ready! 🚀Download the artifacts for: One-line installer (Unstable):bunx flow-debug-build --open 22647803307(execution 22647803307 / attempt 1) |
show() called this.hide() internally to reset state before showing, but hide() now sends 'omnibox:do-hide' to the renderer via IPC. Since callers invoke sendShowEvent() before show(), the hide event was undoing the show event, leaving isVisible=false and the omnibox invisible (opacity 0). Replace this.hide() with a direct view.setVisible(false) to reset the native view without notifying the renderer.
Greptile SummaryThis PR implements Phase 5 of the omnibox redesign, delivering two major architectural improvements — flicker-free preloading and an IPC-based show/hide API — alongside a new scoring engine, default match stability logic, arrow-key navigation lock, URL normalization enhancements, IMUI localStorage caching, and a new omnibox shortcuts persistence layer (SQLite + service + IPC). The changes are well-scoped and represent a meaningful step toward a production-quality omnibox. Key concerns found during review:
Confidence Score: 2/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant MainProcess as Main Process
participant OmniboxWindow as Omnibox (Main)
participant OmniboxRenderer as Omnibox (Renderer)
participant Controller as AutocompleteController
participant Providers as Providers (HQP/Search/Shortcuts/...)
participant DB as SQLite DB
Note over OmniboxWindow: App startup
OmniboxWindow->>OmniboxRenderer: loadInterface() → loads URL (first time only)
OmniboxRenderer-->>OmniboxWindow: did-finish-load → initialLoadComplete=true
User->>MainProcess: Cmd+T / address bar click
MainProcess->>OmniboxWindow: sendShowEvent({currentInput, openIn})
OmniboxWindow->>OmniboxRenderer: IPC: omnibox:do-show (params)
OmniboxRenderer->>OmniboxRenderer: setInput / setIsVisible(true) / focus
OmniboxRenderer->>Controller: handleInput(text, "focus")
Controller->>Providers: start(input) [parallel]
Providers->>DB: history / shortcuts queries
DB-->>Providers: results
Providers-->>Controller: onProviderResults(matches)
alt User presses arrow key
User->>OmniboxRenderer: ArrowUp/ArrowDown
OmniboxRenderer->>Controller: onUserArrowKey()
Controller->>Controller: _userIsNavigating=true
Providers-->>Controller: late results → buffered in pendingMatches
end
alt User types next character
User->>OmniboxRenderer: keystroke
OmniboxRenderer->>Controller: onUserKeystroke(input)
Controller->>Controller: applyPendingUpdates() → updateResults()
Controller->>Controller: start(newInput)
end
Controller-->>OmniboxRenderer: onUpdate(topMatches)
OmniboxRenderer->>OmniboxRenderer: setMatches / render suggestions
User->>OmniboxRenderer: selects suggestion (Enter/click)
OmniboxRenderer->>Controller: openMatch(match, openIn)
Controller->>DB: recordShortcutUsage(input, url, title, type)
User->>MainProcess: closes omnibox (Esc / focus-out)
MainProcess->>OmniboxWindow: hide()
OmniboxWindow->>OmniboxRenderer: IPC: omnibox:do-hide
OmniboxRenderer->>OmniboxRenderer: setIsVisible(false) / stopQuery()
Last reviewed commit: 68bde38 |
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Stale pendingMatches persists across show/hide sessions
stop() (called by Omnibox.stopQuery() when the omnibox is closed) does not clear pendingMatches:
stop(): void {
this.activeProviders = 0;
this._providerTimings = [];
// ← pendingMatches is NOT cleared here
}Similarly, start() does not clear pendingMatches either. This means:
- User opens omnibox → presses arrow keys (
_userIsNavigating = true) → some provider results arrive and are buffered intopendingMatches - User closes omnibox →
stop()is called,_userIsNavigatingstaystrue,pendingMatchesretains stale entries - User opens omnibox again → types a character →
onUserKeystroke()is called →applyPendingUpdates()fires → stale results from the previous session are briefly rendered before the new query completes
pendingMatches should be cleared in stop() (and ideally also in start()) to prevent cross-session contamination:
stop(): void {
this.activeProviders = 0;
this._providerTimings = [];
this.pendingMatches = [];
this._userIsNavigating = false;
}
Additional Comments (3)
If the user presses Cmd+T during the ~200–500 ms window while the omnibox renderer is still loading its initial URL (between the constructor's The fix is to route through // Route through loadInterface so it can handle the not-yet-loaded case:
omnibox.loadInterface({ currentInput: null, openIn: "new_tab" });
omnibox.setBounds(null);
omnibox.show();
The check
The check should be scheme-aware: const port = parsed.port; // already empty for scheme-default ports
const hasNonDefaultPort = port !== ""; // sufficient — URL already handled defaults
const portSuffix = hasNonDefaultPort ? `:${port}` : "";Since
The method docstring says results are "ordered by relevance (hit count decayed by recency)", but the SQL query only orders by Consider a user who typed Either add a secondary sort by .orderBy(desc(schema.omniboxShortcuts.hitCount), desc(schema.omniboxShortcuts.lastAccessTime))Or if recency is intentionally prioritized, update the docstring to reflect that. |
…tion The blur event handler on the omnibox webcontents fires maybeHide() → hide() → sendHideEvent() during or shortly after the show() focus dance. Because sendShowEvent() is called BEFORE show(), the sequence was: renderer receives show (isVisible=true), then blur triggers hide (isVisible=false), leaving the omnibox invisible. The fix: stop sending hide IPC from hide(). Native view visibility (view.setVisible) is the sole mechanism for main-process-initiated hides. The renderer already manages its own isVisible state for user-initiated hides (Escape key, match selection), and the show handler always resets state on next open.
…L navigation For URL-classified inputs (e.g. roblox.com), Google's API often returns high verbatim relevance (>=1300) for popular domains. The server blending logic was letting this override the input-type-based score, pushing verbatim from 1100 to 1300 and beating url-what-you-typed's 1200. Now URL inputs always use the base relevance (1100), ensuring navigation always wins over search for URL-like input.
Summary
Phase 5 of the omnibox redesign, implementing stability & polish features from the design doc plus two major architectural improvements: eliminating omnibox flickering and creating a cleaner IPC-based API.
Changes
Scoring Engine (design doc §6.3-6.4)
scoring-engine.tswith centralizedscoreMatchQuality()andcomputeRelevance()Default Match Stability (design doc §9)
shouldUpdateDefault()logic: new top must exceed current default by >100 pointsupdateResults()— moves existing default to top when stableArrow-Key Navigation Lock (design doc §9.3)
onUserArrowKey()suppresses result list updates while user navigatesonUserKeystroke()clears lock and applies buffered pending matchesOmniboxclass andOmniboxMainUI componentURL Normalization Enhancement (design doc §8)
IMUI Cache/Restore (design doc §4.5)
wordCount,prefixCount,lastRefreshFlicker-Free Omnibox (new)
loadInterface()which reloaded the URL, remounting Reactomnibox:do-show/omnibox:do-hideIPC messagesinitialLoadComplete, routes subsequent shows throughsendShowEvent()IPC-Based API (new)
OmniboxShowParamstype andonShow/onHidetoFlowOmniboxAPIlistenOnIPCChannelOmniboxMainis now a persistent component — no remount on show/hideDebug Page Updates
ScoringSignalsfields with visual layoutFiles Changed
src/renderer/src/lib/omnibox/scoring-engine.tsautocomplete-controller.ts,url-normalizer.ts,in-memory-url-index.ts,omnibox.ts(renderer)omnibox.ts(main process),omnibox.ts(IPC),new-tab.ts,file.ts(menu)omnibox.ts(shared interface),index.ts(preload)main.tsx(omnibox UI),page.tsx(debug)