Skip to content

feat: ship the omnibox refactor#234

Open
iamEvanYT wants to merge 10 commits intomainfrom
evan/omnibox-refactor/main
Open

feat: ship the omnibox refactor#234
iamEvanYT wants to merge 10 commits intomainfrom
evan/omnibox-refactor/main

Conversation

@iamEvanYT
Copy link
Member

@iamEvanYT iamEvanYT commented Mar 10, 2026

Summary

  • deliver the multi-phase omnibox refactor, including tokenized matching, frecency scoring, inline autocomplete, and new history/search/open-tab ranking behavior
  • add persistence and IPC plumbing for history collection and omnibox shortcuts, plus the supporting Drizzle schema and services
  • round out the experience with shortcut and bookmark providers, search relevance improvements, debug tooling, and preload polish for a more stable flicker-free omnibox

What this changes

  • replaces the older omnibox matching flow with a more structured scoring pipeline built around tokenization, URL normalization, frecency, and provider-specific ranking
  • introduces richer result sources across history, open tabs, search, shortcuts, bookmarks, pedals, navsuggest, and zero-suggest so the omnibox can behave more like a browser-native command surface
  • adds main-process persistence for browsing history and omnibox shortcuts, plus the related IPC and preload APIs needed for renderer-side querying and updates
  • improves inline completion, result stability, and preloading behavior to reduce visual churn while typing and make selection behavior more predictable
  • expands the omnibox debug tooling and design docs so it is easier to inspect ranking behavior and continue iterating safely

Why this PR is large

  • this branch intentionally rolls up five sequential omnibox phases that were developed and merged independently, so the combined PR includes both the final UX work and all of the supporting data/model plumbing underneath it
  • a meaningful portion of the diff is foundational code: Drizzle migrations, schema updates, services, provider abstractions, and scoring utilities that enable the visible UX improvements
  • the remaining changes are mostly integration work across Electron main, preload, shared flow interfaces, and renderer omnibox components to make the end-to-end flow work cleanly

Validation

  • bun typecheck

Summary by CodeRabbit

  • New Features

    • Inline omnibox completions with ghost-text and accept-on-Tab/ArrowRight.
    • Learned omnibox shortcuts (recording, search, and cleanup) for faster navigation.
    • Bookmark-backed suggestions and improved search suggestion parsing (including navigation suggestions).
  • Improvements

    • Faster, tokenized history matching with frecency-based ranking.
    • Better deduplication, merged match metadata, and more stable default selection during arrow-key navigation.
    • More reliable show/hide behavior for the omnibox.

iamEvanYT and others added 7 commits February 28, 2026 23:58
…ency scoring (#208)

* feat(omnibox): implement Phase 1 — history collection, tokenized matching, frecency scoring

* chore: formatt
…etion (#209)

* feat(omnibox): Phase 2 — IMUI, HistoryQuickProvider, inline autocompletion

* fix(omnibox): show page title instead of duplicate URL in history-url matches
…vance and navsuggest (#211)

* feat(omnibox): Phase 3 — Search provider enhancement with server relevance and navsuggest

* fix(omnibox): remove unused _input params from SearchProvider private methods
…213)

* feat(omnibox): Phase 4 — Shortcuts provider + BookmarkProvider stub

* fix(omnibox): invert shortcuts search query to match stored prefix against current input

* chore: format
…226)

* feat(omnibox): Phase 4 — Shortcuts provider + BookmarkProvider stub

- 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

* fix(omnibox): invert shortcuts search query to match stored prefix against 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'.

* chore: format

* feat(omnibox): add centralized scoring engine

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.

* feat(omnibox): add default match stability, arrow-key lock, and provider 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

* feat(omnibox): enhance URL normalization for better dedup

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.

* feat(omnibox): add IMUI cache/restore with localStorage persistence

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.

* feat(omnibox): replace reload-based communication with IPC message passing

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()

* feat(omnibox): make omnibox UI persistent with IPC-driven show/hide

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

* feat(omnibox): enhance debug page with scoring, timings, and IMUI panels

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

* fix(omnibox): prevent show() from sending hide IPC event to renderer

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.

* fix(omnibox): remove sendHideEvent from hide() to fix show race condition

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.

* fix(omnibox): prevent server blending from boosting verbatim above URL 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.

* feat: better omnibox styling

* fix: issues

* chore: format

* fix: address Codex review feedback
…/main

# Conflicts:
#	src/renderer/src/components/omnibox/main.tsx
@github-actions
Copy link
Contributor

github-actions bot commented Mar 10, 2026

Build artifacts for all platforms are ready! 🚀

Download the artifacts for:

One-line installer (Unstable):
bunx flow-debug-build@1.2.1 --open 23392875505

(execution 23392875505 / attempt 1)

@greptile-apps
Copy link

greptile-apps bot commented Mar 10, 2026

Greptile Summary

This PR delivers a large, well-designed omnibox refactor: tokenized multi-term matching, frecency scoring with exponential decay, an in-memory URL index (IMUI), inline autocomplete, new history/shortcut/bookmark providers, and the SQLite schema + IPC plumbing to back them. The architecture closely follows the design document and the provider orchestration (requestId guards, default-match stability, arrow-key lock) is implemented correctly. Overall the approach is solid, but there are a few bugs to fix before merging:

  • Double history recordingnavigation:go-to records a TYPED visit and then the tab URL-change listener in tab.ts records the same URL again as LINK. Every typed omnibox navigation inflates visitCount by 2 and permanently sets lastVisitType = LINK, defeating the 4× frecency bonus for typed navigations. The comment on line 579 of tab.ts acknowledges typed navigations are "already recorded," but the deduplication guard was never implemented.
  • No UNIQUE constraint on history.url — The migration creates a plain (non-unique) index. The application-level select-then-upsert guard is not atomic; the double-recording path above is direct evidence it can be bypassed. A UNIQUE INDEX (and ideally a compound unique on omnibox_shortcuts.(input_text, destination_url)) is needed to enforce invariants at the database layer.
  • tokenize() silently drops camelCase boundaries — The function lowercases before splitting, so camelCase info is lost. The code even has an inline comment calling this out ("camelCase boundaries are gone") and a tokenizeWithCamelCase helper exists to fix it — but tokenize() never calls it.
  • Verbatim match missing allowedToBeDefault — The initial synchronous verbatim match from SearchProvider does not set allowedToBeDefault: true, which can affect tie-breaking in the sort and default-match stability logic.

Confidence Score: 2/5

  • Not safe to merge — the double history recording bug will corrupt frecency data for all users from the moment they upgrade.
  • The architecture and most of the implementation are high quality, but the double-recording bug is a data-integrity issue that silently degrades the core frecency signal for every user going forward. Combined with the missing UNIQUE constraint (which means duplicates accumulate without any database-level safeguard) and the tokenizer bug (which reduces recall quality), these need to be resolved before shipping.
  • src/main/controllers/tabs-controller/tab.ts (double recording), src/main/ipc/browser/navigation.ts (typed recording with empty title), src/main/saving/db/schema.ts + drizzle/0001_huge_the_leader.sql (missing UNIQUE constraint), src/renderer/src/lib/omnibox/tokenizer.ts (camelCase bug).

Important Files Changed

Filename Overview
src/main/controllers/tabs-controller/tab.ts Introduces history recording on URL change, but every typed omnibox navigation fires two recordVisit calls (once in navigation:go-to as TYPED and once here as LINK), inflating visitCount and always overwriting lastVisitType to LINK — breaking frecency accuracy.
src/main/saving/db/schema.ts Adds history, omniboxShortcuts, and bookmarks tables. The history.url and omniboxShortcuts.(inputText, destinationUrl) columns lack UNIQUE database constraints; the application-level select-then-upsert guard is non-atomic, allowing duplicate rows on concurrent writes.
src/renderer/src/lib/omnibox/tokenizer.ts New tokenizer module. The main tokenize() function lowercases input before camelCase splitting, losing all camelCase boundaries — a bug acknowledged in an inline comment but never fixed; tokenizeWithCamelCase() exists but is not used by tokenize().
src/main/saving/history/history-service.ts Well-structured service with clear query methods and proper error handling; select-then-upsert logic is correct for single-writer usage but relies on application-level uniqueness rather than a DB constraint.
src/renderer/src/lib/omnibox/providers/search.ts Solid enhancement with server relevance parsing, AbortController-based cancellation, and 50 ms debounce. Missing allowedToBeDefault on the verbatim match may slightly affect default-match stability ordering.
src/renderer/src/lib/omnibox/providers/history-quick.ts Well-implemented synchronous IMUI-backed provider with inline completion, correct relevance computation, and proper scoring signal population.
src/renderer/src/lib/omnibox/autocomplete-controller.ts Clean orchestrator with robust requestId-based stale-result rejection, default-match stability enforcement, and arrow-key navigation lock. Provider timing diagnostics are a useful addition.
src/renderer/src/lib/omnibox/in-memory-url-index.ts Correct IMUI implementation with localStorage cache, incremental update support, and a multi-term intersection query algorithm. Diagnostic getters are useful for the debug page.
src/renderer/src/components/omnibox/main.tsx Mostly clean React component integrating the new Omnibox API with inline completion ghost text rendering. Uses destinationUrl as React list key, which risks duplicates if deduplication is incomplete.
src/main/ipc/browser/navigation.ts Records the typed visit with an empty title at navigation time before the page loads. Combined with the tab URL-change listener in tab.ts also recording, this causes double-counting of typed navigations.

Sequence Diagram

sequenceDiagram
    participant User
    participant OmniboxUI as OmniboxUI (Renderer)
    participant Omnibox as Omnibox.ts
    participant Controller as AutocompleteController
    participant HQP as HistoryQuickProvider (sync)
    participant HUP as HistoryURLProvider (async)
    participant Search as SearchProvider (async)
    participant IMUI as InMemoryURLIndex
    participant IPC as Electron IPC
    participant Main as Main Process
    participant DB as SQLite DB

    User->>OmniboxUI: Types in omnibox
    OmniboxUI->>Omnibox: handleInput(text, "keystroke")
    Omnibox->>Controller: onUserKeystroke(input)
    Controller->>Controller: stop() — cancel previous query, reset requestId
    Controller->>HQP: start(input, callback)
    HQP->>IMUI: query(terms)
    IMUI-->>HQP: IMUIQueryResult[]
    HQP-->>Controller: onResults(matches) [sync]
    Controller->>HUP: start(input, callback)
    HUP-->>Controller: onResults([whatYouTypedMatch], continuous=true) [sync]
    Controller->>Search: start(input, callback)
    Search-->>Controller: onResults([verbatimMatch], continuous=true) [sync]
    Controller->>OmniboxUI: onUpdate(topMatches) [initial render]

    Note over HUP,DB: Async DB path
    HUP->>IPC: flow.history.getSignificant()
    IPC->>Main: history:get-significant
    Main->>DB: SELECT significant rows
    DB-->>Main: HistoryRow[]
    Main-->>IPC: HistoryRow[]
    IPC-->>HUP: HistoryEntry[]
    HUP->>HUP: matchHistory() — score & rank
    HUP-->>Controller: onResults(historyMatches)
    Controller->>OmniboxUI: onUpdate(topMatches) [updated]

    Note over Search,Main: Async search suggestions
    Search->>Main: Google Suggest API (fetch, debounced 50ms)
    Main-->>Search: ParsedSuggestion[]
    Search-->>Controller: onResults([updatedVerbatim, ...suggestions])
    Controller->>OmniboxUI: onUpdate(topMatches) [final]

    User->>OmniboxUI: Presses Enter / selects match
    OmniboxUI->>Omnibox: openMatch(match, whereToOpen)
    Omnibox->>IPC: flow.omniboxShortcuts.recordUsage(...)
    Omnibox->>IPC: flow.navigation.goTo(url)
    IPC->>Main: navigation:go-to
    Main->>DB: recordVisit(url, "", TYPED)
    Main->>Main: tab.loadURL(url)
    Note over Main,DB: ⚠️ Tab URL change also fires recordVisit(url, title, LINK) — double count
Loading

Last reviewed commit: 87de30d

Comment on lines +578 to +584
// Record non-typed navigation in history.
// Typed navigations are already recorded in navigation:go-to IPC handler.
// We record here with LINK type as a fallback for link-clicks, redirects, etc.
// We use the current title (may be stale; will be updated on next title change).
if (newUrl && !newUrl.startsWith("about:")) {
historyService.recordVisit(newUrl, newTitle || this.title, VisitType.LINK);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double history recording inflates visitCount and corrupts lastVisitType

The comment on line 579 correctly notes that typed navigations are already recorded in navigation:go-to, but the guard was never implemented. As a result, every omnibox navigation fires two recordVisit calls for the same URL:

  1. navigation:go-torecordVisit(url, "", VisitType.TYPED) — inserts a new row with visitCount=1, typedCount=1
  2. Tab URL change here → recordVisit(newUrl, title, VisitType.LINK) — increments the same row to visitCount=2, typedCount=1, lastVisitType=LINK

Consequences for frecency scoring:

  • visitCount is double-counted for every omnibox navigation (inflated by 2× vs. the true number of navigations)
  • lastVisitType is always overwritten to LINK (weight 1.0) instead of TYPED (weight 4.0), so typed-URL frecency bonuses are never awarded

The fix is to skip history recording here when the navigation was typed. Since detecting the origin in tab.ts is tricky, one approach is to only record in this listener when the URL was not already just recorded (e.g., check a small in-flight set managed by the tabs controller, or simply not record in navigation:go-to and rely on this listener for all visits, passing the typed flag via a tab attribute).

Comment on lines +77 to +92
export const history = sqliteTable(
"history",
{
id: integer("id").primaryKey({ autoIncrement: true }),
url: text("url").notNull(),
title: text("title").notNull().default(""),
visitCount: integer("visit_count").notNull().default(1),
typedCount: integer("typed_count").notNull().default(0),
lastVisitTime: integer("last_visit_time").notNull(), // epoch ms
firstVisitTime: integer("first_visit_time").notNull(), // epoch ms
// Bitfield: 0=link, 1=typed, 2=bookmark, 3=redirect, 4=reload
lastVisitType: integer("last_visit_type").notNull().default(0)
},
(table) => [
index("idx_history_url").on(table.url),
index("idx_history_last_visit").on(table.lastVisitTime),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-unique index on history.url allows duplicate URL rows

The idx_history_url index is created as a plain (non-unique) index. The upsert logic in HistoryService.recordVisit relies on a select-then-insert/update pattern, but this is not atomic. Two concurrent writes for the same URL (which happens in practice because navigation:go-to and tab.ts both call recordVisit for every typed navigation) can both read an empty result before either has committed, resulting in two separate rows for the same URL.

Both the history table and omnibox_shortcuts table have the same issue (no unique constraint on their logical keys).

The schema should enforce uniqueness at the database level:

// history table
(table) => [
  uniqueIndex("idx_history_url").on(table.url), // ← unique
  index("idx_history_last_visit").on(table.lastVisitTime),
  index("idx_history_typed_count").on(table.typedCount)
]

// omnibox_shortcuts table  
(table) => [
  index("idx_omnibox_shortcuts_input").on(table.inputText),
  uniqueIndex("idx_omnibox_shortcuts_destination_input") // ← compound unique on (input_text, destination_url)
    .on(table.inputText, table.destinationUrl),
]

A new Drizzle migration would be needed after changing these.

Comment on lines +26 to +46

// Split camelCase: insert boundary before uppercase letters
// Since we already lowercased, we need to do this on the original
// Actually, since we lowercased first, camelCase boundaries are gone.
// Let's handle this differently: split on the original, then lowercase.
tokens.push(part);
}

return tokens;
}

/**
* Tokenize with camelCase splitting (for cases where we want finer granularity).
*/
export function tokenizeWithCamelCase(input: string): string[] {
if (!input) return [];

// Split camelCase first, then split on non-alphanumeric
const withCamelSplit = input.replace(/([a-z])([A-Z])/g, "$1 $2");
return tokenize(withCamelSplit);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CamelCase splitting is silently broken — acknowledged in a comment but never fixed

The function lowercases the string on line 33 before any camelCase processing. The comment in the loop body explicitly acknowledges the problem:

"Since we already lowercased, camelCase boundaries are gone."

The tokenizeWithCamelCase function below correctly handles this by applying replace(/([a-z])([A-Z])/g, ...) before lowercasing, but tokenize() never calls it. As a result:

  • A title like "JavaScriptReference" tokenizes as ["javascriptreference"] (one token) instead of ["javascript", "reference"]
  • A URL path like /myApiEndpoint tokenizes as ["myapiendpoint"] instead of ["my", "api", "endpoint"]

This hurts recall for history/shortcut/bookmark matching in HQP and HUP. The fix is to apply the camelCase split before lowercasing:

export function tokenize(input: string): string[] {
  if (!input) return [];
  // Split camelCase BEFORE lowercasing to preserve boundaries
  const withCamelSplit = input.replace(/([a-z])([A-Z])/g, "$1 $2");
  const parts = withCamelSplit.toLowerCase().split(/[^a-z0-9]+/);
  return parts.filter((p) => p.length > 0);
}

Comment on lines 128 to 151
const verbatimMatch: AutocompleteMatch = {
providerName: this.name,
relevance: url ? 1250 : 1300, // High score to appear near top, but below strong nav
relevance: verbatimRelevance,
contents: inputText,
description: `Search for "${inputText}"`, // Or search engine name
description: `Search for "${inputText}"`,
destinationUrl: createSearchUrl(inputText),
type: "verbatim", // Special type for clarity, often treated as search
isDefault: true // Usually the fallback default action
type: "verbatim",
isDefault: input.inputType !== InputType.URL,
scoringSignals: {
typedCount: 0,
visitCount: 0,
elapsedTimeSinceLastVisit: 0,
frecency: 0,
matchQualityScore: 0,
hostMatchAtWordBoundary: false,
hasNonSchemeWwwMatch: false,
isHostOnly: false,
isBookmarked: false,
hasOpenTabMatch: false,
urlLength: 0,
isVerbatim: true,
searchSuggestRelevance: verbatimRelevance
}
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verbatim AutocompleteMatch is missing allowedToBeDefault

The verbatim match emitted at line 128 does not set allowedToBeDefault. In AutocompleteResult.sort(), when two matches have equal relevance, matches with allowedToBeDefault: true are sorted above those without. In AutocompleteController.updateResults(), the short-input caution check also reads newTop.allowedToBeDefault (implicitly via shouldUpdateDefault). Setting this field ensures the verbatim match participates correctly in stability decisions.

Suggested change
const verbatimMatch: AutocompleteMatch = {
providerName: this.name,
relevance: url ? 1250 : 1300, // High score to appear near top, but below strong nav
relevance: verbatimRelevance,
contents: inputText,
description: `Search for "${inputText}"`, // Or search engine name
description: `Search for "${inputText}"`,
destinationUrl: createSearchUrl(inputText),
type: "verbatim", // Special type for clarity, often treated as search
isDefault: true // Usually the fallback default action
type: "verbatim",
isDefault: input.inputType !== InputType.URL,
scoringSignals: {
typedCount: 0,
visitCount: 0,
elapsedTimeSinceLastVisit: 0,
frecency: 0,
matchQualityScore: 0,
hostMatchAtWordBoundary: false,
hasNonSchemeWwwMatch: false,
isHostOnly: false,
isBookmarked: false,
hasOpenTabMatch: false,
urlLength: 0,
isVerbatim: true,
searchSuggestRelevance: verbatimRelevance
}
};
const verbatimMatch: AutocompleteMatch = {
providerName: this.name,
relevance: verbatimRelevance,
contents: inputText,
description: `Search for "${inputText}"`,
destinationUrl: createSearchUrl(inputText),
type: "verbatim",
isDefault: input.inputType !== InputType.URL,
allowedToBeDefault: input.inputType !== InputType.URL,
scoringSignals: {

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 22, 2026

Walkthrough

Implements a full omnibox redesign: new DB schema and shortcuts service, main↔renderer IPC show/hide readiness flow, renderer-side IMUI (in-memory URL index), tokenizer/frecency/scoring engine, multiple providers (history quick, shortcuts, bookmarks, search, etc.), deduplication, and inline completion support.

Changes

Cohort / File(s) Summary
Design Doc
design/omnibox-redesign.md
Added comprehensive omnibox design and implementation plan.
Database & Migrations
drizzle/0001_tough_scream.sql, drizzle/0003_omnibox_shortcuts.sql, drizzle/meta/_journal.json, src/main/saving/db/schema.ts
Added omnibox_shortcuts migration, journal entry, minor EOF fix, and Drizzle table/types for shortcuts.
Main process & IPC show/hide
src/main/controllers/.../omnibox.ts, src/main/ipc/window/omnibox.ts, src/main/ipc/app/new-tab.ts, src/main/controllers/app-menu-controller/menu/items/file.ts
Introduced renderer-ready queuing, sendShowEvent/sendHideEvent/markRendererReady, replaced some loadInterface calls with show-event flow.
Shortcuts backend & IPC handlers
src/main/saving/omnibox-shortcuts/shortcuts-service.ts, src/main/ipc/data/omnibox-shortcuts.ts, src/main/ipc/index.ts
New OmniboxShortcutsService with record/search/cleanup and IPC handlers registered.
Preload & shared types
src/preload/index.ts, src/shared/flow/.../omnibox.ts, src/shared/flow/.../omnibox-shortcuts.ts, src/shared/flow/flow.ts
Exposed flow.browser.omniboxShortcuts API; added OmniboxShowParams, OmniboxShortcuts interfaces; added onShow/onHide subscriptions and global typing.
Renderer core types & utils
src/renderer/src/lib/omnibox/types.ts, tokenizer.ts, url-normalizer.ts, frecency.ts, scoring-engine.ts, search.ts
New input/match types, tokenizers, URL normalization, frecency calculations, scoring engine, and Google suggestions parsing.
In-memory index
src/renderer/src/lib/omnibox/in-memory-url-index.ts
New IMUI class with token/prefix inverted indexes, populate/refresh/add/update/query APIs and localStorage snapshotting.
Providers & data sources
src/renderer/src/lib/omnibox/providers/..., src/renderer/src/lib/omnibox/data-providers/*, src/renderer/src/lib/omnibox/shortcuts.ts
Added/updated providers: HistoryQuick (IMUI-backed), HistoryURL, Shortcuts, Bookmark stub, Search (metadata-aware), OpenTab, Pedal, ZeroSuggest; added renderer-side data-provider functions and shortcut recording.
Controller, merging, dedupe, inline completion
src/renderer/src/lib/omnibox/autocomplete-controller.ts, autocomplete-result.ts, omnibox.ts, tokenizer.ts
Default-match stability, navigation-lock buffering, provider timings, dedupKey-based merging, inline-completion pipeline, IMUI lifecycle and provider wiring.
UI & debug
src/renderer/src/components/omnibox/main.tsx, src/renderer/src/routes/omnibox-debug/page.tsx
IPC-driven show/hide listeners, inline ghost-text, keyboard handling, IMUI/provider diagnostics surfaced in debug UI.
History retrieval change
src/main/saving/history/browsing-history.ts
Increased history list limit from 200 → 2000 for IMUI population.
Misc
src/renderer/src/lib/omnibox/data-providers/history.ts, bookmarks.ts
Refactored history provider APIs (search/significant/recent/most-visited) and added bookmark provider stubs.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Renderer as Renderer Process
    participant Main as Main Process
    participant DB as DB/Shortcuts

    User->>Renderer: trigger omnibox (click/Cmd+K)
    Renderer->>Main: omnibox:show IPC (currentInput, openIn)
    Main->>Main: sendShowEvent() (queue if renderer not ready)
    Main->>Renderer: omnibox:do-show IPC
    Renderer->>Main: omnibox:renderer-ready IPC
    Main->>Main: markRendererReady() (flush queued shows)
    Renderer->>Renderer: render omnibox UI
    User->>Renderer: type input
    Renderer->>Renderer: classify/tokenize, query IMUI
    Renderer->>DB: omnibox-shortcuts:search IPC
    DB-->>Renderer: shortcuts results
    Renderer->>Renderer: providers start, score, merge, dedupe, inline completion
    Renderer->>Renderer: display suggestions
Loading
sequenceDiagram
    participant User
    participant Renderer as Omnibox (Renderer)
    participant IMUI as InMemoryURLIndex
    participant Providers as Data Providers
    participant Main as Main Process

    User->>Renderer: keystroke
    Renderer->>Renderer: classifyInput(), tokenizeInput()
    Renderer->>IMUI: query(terms)
    IMUI-->>Renderer: IMUIQueryResult[]
    Renderer->>Providers: start() (HistoryQuick, Search, Shortcuts, etc.)
    Providers->>Main: omnibox-shortcuts:search (if needed)
    Main-->>Providers: results
    Providers-->>Renderer: onResults(matches[])
    Renderer->>Renderer: merge/dedup via dedupKey, compute relevance, choose default, produce inline completion
    Renderer->>Renderer: render updated suggestion list
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Major Rewrite #20 — modifies Omnibox IPC/show-hide flow and renderer readiness handling.
  • Redesign #2 — earlier Omnibox implementation and wiring; overlaps in IPC, preload, and main/renderer integration.
  • feat: add history feature #240 — changes to browsing-history APIs used by the IMUI and history providers.

Suggested labels

enhancement

Poem

🐰 A little rabbit hops to cheer,

Tokens scurry, frecency clear,
Shortcuts learn and indexes hum,
Inline ghosts whisper what's to come,
Omnibox blooms — hop, type, and run! 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: ship the omnibox refactor' clearly describes the main objective of this large changeset, which is delivering a comprehensive omnibox refactor. It directly reflects the primary intent stated in the PR objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch evan/omnibox-refactor/main

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 34

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/renderer/src/routes/omnibox-debug/page.tsx (1)

273-275: ⚠️ Potential issue | 🟡 Minor

String interpolation bug - input variable not interpolated.

The string literal {'No suggestions found for "{input}"'} doesn't use template literal syntax, so input is displayed literally instead of the actual input value.

🐛 Proposed fix
-                    {'No suggestions found for "{input}"'}
+                    {`No suggestions found for "${input}"`}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/routes/omnibox-debug/page.tsx` around lines 273 - 275, The
displayed message is using a plain string so the input variable isn't
interpolated; update the JSX in the omnibox-debug page where the Search icon and
message are rendered (the block containing Search and the text 'No suggestions
found for "{input}"') to interpolate the input variable — e.g., replace the
quoted literal with a JSX expression using a template literal or string
concatenation (use `\`No suggestions found for "${input}"\`` or `'No suggestions
found for "' + input + '"'`) so the actual value of the input variable is shown.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@design/omnibox-redesign.md`:
- Around line 38-74: The fenced ASCII diagram containing nodes like "User
Input", "InputClassifier", "AutocompleteController", "AutocompleteResult",
"ScoringEngine", and "UI (Dropdown + Inline Complete)" should have a language
specifier (use ```text or ```plaintext) to satisfy markdown linting; update that
diagram block accordingly and apply the same fix to all other
pseudocode/file-list fenced blocks (the other blocks showing algorithm steps or
file lists) by adding an appropriate language tag such as text or plaintext so
syntax highlighting and lint checks pass.

In `@drizzle/0003_omnibox_shortcuts.sql`:
- Around line 1-9: The `pinned_tabs` CREATE TABLE in 0003_omnibox_shortcuts.sql
duplicates the same table from 0001_tough_scream.sql; either remove the
duplicate CREATE TABLE line from 0003_omnibox_shortcuts.sql (keeping only the
index creation idx_pinned_tabs_profile_id), or if you must keep the CREATE for
safety, add a clear comment in 0003_omnibox_shortcuts.sql stating that the table
is also defined in 0001_tough_scream.sql and that IF NOT EXISTS is used to
ensure idempotency—referencing `pinned_tabs`, `idx_pinned_tabs_profile_id`,
0003_omnibox_shortcuts.sql and 0001_tough_scream.sql so reviewers can track the
intent.
- Around line 11-23: Add an index on omnibox_shortcuts.last_access_time to speed
cleanup and recency ordering; update the migration to create an index such as
idx_omnibox_shortcuts_last_access_time ON `omnibox_shortcuts`
(`last_access_time`) so queries using .where(lastAccessTime < cutoff) and ORDER
BY last_access_time use the index.

In `@src/main/controllers/app-menu-controller/menu/items/file.ts`:
- Around line 50-53: Reorder the omnibox call sequence to match the project
convention: call omnibox.sendShowEvent(...) before omnibox.setBounds(...), and
then call omnibox.show(); specifically update the block that currently calls
omnibox.setBounds(null); omnibox.sendShowEvent({ currentInput: null, openIn:
"new_tab" }); omnibox.show(); so it becomes sendShowEvent -> setBounds -> show
using the omnibox.sendShowEvent, omnibox.setBounds, and omnibox.show methods.

In `@src/main/controllers/windows-controller/utils/browser/omnibox.ts`:
- Around line 157-160: The code must drop queued show payloads when a hide races
ahead: when hiding the omnibox (the method that dispatches
"omnibox:do-hide"/sets hidden state), clear pendingShowParams so stale show
requests won't be replayed; additionally, update markRendererReady() to check
the current hidden/shown state before flushing pendingShowParams and only emit
"omnibox:do-show" if the omnibox is still expected to be visible. In short,
clear pendingShowParams in the hide handler and gate the pendingShowParams flush
in markRendererReady() by checking the same visibility flag used for hide/show
to avoid replaying stale payloads.

In `@src/main/ipc/data/omnibox-shortcuts.ts`:
- Around line 18-23: The IPC handler registered with ipcMain.on for
"omnibox-shortcuts:record-usage" currently forwards renderer-provided parameters
directly to omniboxShortcutsService.recordUsage; add defensive validation at
that boundary: verify inputText, destinationUrl, destinationTitle, and matchType
are strings (and not excessively long), ensure inputText/destinationTitle are
non-empty, validate/sanitize destinationUrl (e.g., URL.parse/try-catch or
whitelist schemes like http/https), and ensure matchType is one of the expected
enum/values before calling omniboxShortcutsService.recordUsage; if validation
fails, log the rejection and return early (do not call the service).

In `@src/main/ipc/window/omnibox.ts`:
- Around line 48-59: The method sendHideEvent() in the Omnibox class is dead
code (defined but never called); remove the entire sendHideEvent()
implementation and any attendant unused imports or references, and keep the
hide() method comment/behavior as-is to preserve the intentional race-condition
avoidance; search for sendHideEvent, hide, and Omnibox to locate the code to
delete and ensure no remaining references or unused variables remain after
removal.
- Around line 23-27: The cast on params?.openIn is unsafe: instead of asserting
(params?.openIn as "current" | "new_tab"), validate the incoming value before
passing to omnibox.sendShowEvent; check params?.openIn against the allowed
strings "current" and "new_tab" (e.g., const openIn = params?.openIn ===
"current" ? "current" : "new_tab") and pass that validated/fallback value into
omnibox.sendShowEvent along with currentInput to avoid accepting malformed IPC
data.

In `@src/main/saving/db/schema.ts`:
- Around line 132-147: The omnibox_shortcuts schema (omniboxShortcuts) lacks a
database-level uniqueness constraint on (inputText, destinationUrl), so add a
unique constraint/index on those two columns to enforce uniqueness at the DB
level; update the sqliteTable definition for omniboxShortcuts to include a
unique composite index/constraint on table.inputText and table.destinationUrl
(so recordUsage's upsert logic remains compatible but the DB prevents duplicates
if application logic changes).

In `@src/main/saving/omnibox-shortcuts/shortcuts-service.ts`:
- Around line 82-90: The current WHERE clause uses SQL LIKE which treats stored
inputText values as patterns and lets SQL wildcards (%) and (_) alter matching;
update the predicate in shortcuts-service.ts (the query using normalizedInput
and schema.omniboxShortcuts.inputText) to use instr(normalizedInput,
schema.omniboxShortcuts.inputText) = 1 so you get true prefix matches without
wildcard interpretation, keeping the same ORDER BY on hitCount and
lastAccessTime.

In `@src/renderer/src/components/omnibox/main.tsx`:
- Around line 248-254: The current key handler accepts inline completions on Tab
or ArrowRight which hijacks normal Tab navigation; change the handler in the
block that checks event.key and inlineCompletion (the logic around
inputBox.selectionStart, inlineCompletion, and acceptInlineCompletion) so Tab is
no longer used alone to accept completions—either remove "Tab" from the
condition and only accept on "ArrowRight" at end-of-input, or require a modifier
with Tab (e.g., event.key === "Tab" && event.ctrlKey/altKey/metaKey) before
calling acceptInlineCompletion(); ensure event.preventDefault() and
acceptInlineCompletion() remain only for the chosen key+modifier combination and
preserve the atEnd check using inputBox.selectionStart ===
inputBox.value.length.

In `@src/renderer/src/lib/omnibox/autocomplete-controller.ts`:
- Around line 227-228: The current code calls this.currentResult.deduplicate()
before this.currentResult.sort(), but AutocompleteResult.deduplicate() expects
results to already be relevance-sorted; reverse the calls so you call
this.currentResult.sort() first and then this.currentResult.deduplicate().
Update the sequence around the AutocompleteResult handling (the methods named
sort() and deduplicate() on this.currentResult) so deduplicate runs on a
pre-sorted list.
- Around line 132-135: onUserKeystroke currently calls applyPendingUpdates()
which flushes buffered matches for the previous query and causes a stale-results
flash just before start() clears them; remove the applyPendingUpdates() call
from onUserKeystroke (or guard it so it only runs for navigation/commit events)
so that onUserKeystroke only sets _userIsNavigating, then calls start(input)
without replaying buffered matches; update any tests or callers expecting the
previous flush behavior accordingly and keep references to onUserKeystroke,
applyPendingUpdates, and start to locate the change.
- Around line 230-271: The current logic can leave multiple matches with
isDefault=true when a later provider outranks an earlier verbatim/default; fix
by clearing stale default flags on all candidates before promoting a new
default. In the block that handles topMatches (after const topMatches =
this.currentResult.getTopMatches()), iterate over topMatches and set m.isDefault
= false to reset prior defaults, then apply the existing promotion logic that
sets topMatches[0].isDefault = true or marks the moved existing default; update
references in the defaultMatch/shouldUpdateDefault flow so no old match retains
isDefault.

In `@src/renderer/src/lib/omnibox/autocomplete-result.ts`:
- Around line 56-62: In mergeMatches, when marking the winner as bookmarked you
currently only set winner.scoringSignals.isBookmarked if scoringSignals exists,
which drops the flag when it's missing; update mergeMatches (the method named
mergeMatches and its use of winner.scoringSignals) to ensure
winner.scoringSignals is initialized (create a scoringSignals object with
necessary defaults) when absent before setting isBookmarked=true so the bookmark
status from the duplicate is not lost.
- Around line 34-37: The deduplication mismatch stems from inconsistent dedupKey
generation: normalizeUrlForDedup() (used in autocomplete-result.ts during key =
match.dedupKey ?? normalizeUrlForDedup(match.destinationUrl)) lowercases non-URL
destinationUrl values while some providers (zero-suggest.ts) set dedupKey like
'tab:${tab.id}' and others (open-tab.ts) leave dedupKey undefined and rely on
normalizeUrlForDedup() producing a different key; to fix, standardize dedup keys
for non-URL match types by updating the providers that create open-tab matches
to set an explicit dedupKey using the same format as zero-suggest (e.g.,
'tab:${tab.id}' or include spaceId if needed), and/or adjust
normalizeUrlForDedup() to return a stable, type-prefixed key for non-URL
destinationUrl values so autocomplete-result.ts's uniqueMatches lookup
consistently deduplicates identical tabs across providers (update open-tab.ts,
zero-suggest.ts, and normalizeUrlForDedup to use the same key format).

In `@src/renderer/src/lib/omnibox/data-providers/bookmarks.ts`:
- Around line 1-60: Create a tracking issue to implement the bookmarks service +
renderer IPC and wire up the stubbed functions; include tasks to (1) ensure the
BookmarkEntry type matches the DB schema, (2) implement searchBookmarks to call
flow.bookmarks.search(query, limit), (3) implement getAllBookmarks to call
flow.bookmarks.getAll() for building an in-memory index, and (4) implement
isUrlBookmarked as a fast lookup (e.g., Set<string> refreshed periodically) and
update BookmarkProvider to use these functions.

In `@src/renderer/src/lib/omnibox/data-providers/history.ts`:
- Around line 8-10: getHistory currently always returns the full dataset (via
flow.history.list()) forcing callers like searchHistory, getRecentHistory and
getMostVisitedHistory to materialize the entire history; change the abstraction
so history queries are served with server-side limits/filters: either add
parameters to getHistory (e.g., getHistory({ limit, query, sort })) or create
dedicated RPC-backed helpers (e.g., flow.history.search(query, limit),
flow.history.listRecent(limit), flow.history.listMostVisited(limit)) and update
searchHistory, getRecentHistory and getMostVisitedHistory to call those new
parameterized functions instead of calling getHistory(); keep references to
getHistory, searchHistory, getRecentHistory, getMostVisitedHistory and
flow.history.list when making the change so you update all call sites.

In `@src/renderer/src/lib/omnibox/data-providers/shortcuts.ts`:
- Around line 28-32: The try/catch around flow.omniboxShortcuts.recordUsage in
the recordShortcutUsage path gives a false impression it will catch IPC errors
(which happen async in the main process); instead either remove the try/catch
and perform a defensive existence check or change the catch message to
explicitly state it only catches synchronous/local errors. Locate the
recordShortcutUsage caller and the flow.omniboxShortcuts.recordUsage call and:
1) replace the try/catch with a guard like if
(flow?.omniboxShortcuts?.recordUsage) flow.omniboxShortcuts.recordUsage(...); or
2) keep the catch but update the console.error text to say "synchronous/local
error while attempting to send IPC (main process errors are handled there):" so
logs aren't misleading. Ensure references to ipcRenderer.send remain untouched
since IPC errors are handled in main.

In `@src/renderer/src/lib/omnibox/in-memory-url-index.ts`:
- Around line 160-171: addOrUpdate() currently always inserts new IDs and
ignores MAX_ENTRIES, letting the in-memory index grow unbounded; modify
addOrUpdate (and related paths like buildEntry/addToIndexes/removeFromIndexes)
to enforce the same MAX_ENTRIES policy used by rebuild()/restore(): before
inserting a new id, check if this.entries.size >= MAX_ENTRIES and if so evict
the oldest entry(s) (remove from this.entries and call removeFromIndexes on
them) or skip inserting new items, then proceed to buildEntry and addToIndexes;
ensure updates to existing ids still re-index without counting against the cap.

In `@src/renderer/src/lib/omnibox/omnibox.ts`:
- Around line 44-46: The forced-search branch classifies inputs starting with
"?" as InputType.FORCED_QUERY but leaves the leading "?" in the text sent
downstream (e.g., into AutocompleteInput.text), which breaks browser-style
forced-search behavior; update the logic in omnibox.ts (the forced-search
handling where trimmed.startsWith("?") is checked, and the similar block around
lines 232-238) to normalize the input by stripping the leading "?" before
constructing/returning the AutocompleteInput (and anywhere the raw text is
passed on), so the classification remains FORCED_QUERY but the downstream text
is the query without the leading question mark.

In `@src/renderer/src/lib/omnibox/providers/history-quick.ts`:
- Around line 265-267: The allowedToBeDefault comparison is inconsistent with
HistoryURLProvider: here allowedToBeDefault uses >= INLINE_COMPLETION_THRESHOLD
while HistoryURLProvider uses > INLINE_COMPLETION_THRESHOLD; update the
comparison so both providers use the same operator (e.g., change
allowedToBeDefault to relevance > INLINE_COMPLETION_THRESHOLD to match
HistoryURLProvider) and ensure INLINE_COMPLETION_THRESHOLD logic remains
consistent across inlineCompletion, isDefault, allowedToBeDefault in the
HistoryURLProvider and this file (symbols: allowedToBeDefault,
INLINE_COMPLETION_THRESHOLD, HistoryURLProvider, inlineCompletion, isDefault).
- Around line 60-68: The current classification loop using
parsedHost.includes(token) misclassifies tokens that appear in both host and
path (e.g., "com"); update the logic in the area that builds hostTokens and
pathTokens (the loop that iterates entry.urlTokens using parsedHost) to instead
parse the URL once (e.g., create a URL object from entry.url or reuse the
existing parsedHost/path components) and produce separate token lists by
tokenizing the host and the pathname independently from their source strings;
ensure you derive host tokens from the host string and path tokens from the
pathname string rather than checking includes against parsedHost so tokens are
correctly assigned.
- Around line 240-244: The code recomputes scoreMatchQuality(terms, entry) when
building AutocompleteMatch objects; fix this by adding matchQuality to the
scored item structure where scoreMatchQuality is first computed (the "scored"
array), propagate that property through any filtering/sorting into "top", and
then use the carried matchQuality when mapping top to AutocompleteMatch instead
of calling scoreMatchQuality again; update the destructuring in the top.map
callback to read matchQuality from the item (and remove the second call to
scoreMatchQuality).

In `@src/renderer/src/lib/omnibox/providers/history-url.ts`:
- Around line 200-248: The cache and DB paths duplicate the scoring/relevance
logic; extract a shared helper (e.g., computeHistoryResult or
buildHistoryResult) that accepts the history entry, terms, inputText and
provider name and returns the result object (including relevance, contents,
description, destinationUrl, inlineCompletion from getInlineCompletion, dedupKey
from normalizeUrlForDedup, and scoringSignals including frecency from
calculateFrecency, matchQuality from scoreMatchQuality,
elapsedTimeSinceLastVisit as Date.now() - entry.lastVisitTime, urlLength, etc.).
Replace the inline scoring block in both the cache branch and the searchHistory
loop to call this helper for each entry, then push the returned result into
results and keep the single results.sort((a,b)=>b.relevance-a.relevance)
afterward. Ensure the helper reuses tokenize/allTermsMatch checks or returns
null when terms don’t match so callers can skip adding non-matching entries.
- Around line 203-206: The deduplication check inside the dbResults loop is
comparing r.destinationUrl to entry.url, but results store
normalizeUrlForDedup(entry.url) in dedupKey; update the check to compare dedup
keys instead: compute const entryKey = normalizeUrlForDedup(entry.url) (or call
normalizeUrlForDedup inline) and replace the condition if (results.some((r) =>
r.destinationUrl === entry.url)) with if (results.some((r) => r.dedupKey ===
entryKey)); this ensures duplicates with different URL formatting are correctly
filtered (see normalizeUrlForDedup and the loop over dbResults/results).
- Around line 70-81: getInlineCompletion calls stripSchemeAndWww(url) twice;
compute it once and reuse the result. In function getInlineCompletion, keep
inputLower and stripped (from stripSchemeAndWww(url)), use stripped for both the
startsWith check and to compute the completion (return
stripped.slice(inputLower.length)) instead of re-calling stripSchemeAndWww and
using fullStripped.

In `@src/renderer/src/lib/omnibox/providers/open-tab.ts`:
- Around line 66-74: The open-tab results currently set isDefault: true
unconditionally in the results.push call (see results.push in open-tab.ts and
the relevance variable), which conflicts with the controller's expectation that
only the top match is default; change that call to set isDefault
conditionally—either compute isDefault as (relevance > 1300) like
history-url.ts/history-quick.ts or only mark the highest-relevance tab as
default—so update the results.push object to set isDefault based on relevance
(or after ranking) rather than always true.
- Around line 26-77: The call to getOpenTabsInSpace() in the open-tab provider
lacks error handling; wrap its promise with a .catch() (or use try/catch if
converted to async) to handle rejections, log the error and ensure onResults is
invoked (e.g., onResults([]) or a fallback) so the provider doesn't hang; update
the block around getOpenTabsInSpace(), the results handling and any logging
(referencing getOpenTabsInSpace, onResults, this.name) to ensure failures are
caught, a useful error is logged, and onResults is always called.

In `@src/renderer/src/lib/omnibox/providers/search.ts`:
- Around line 248-254: When buildNavSuggestMatch(suggestion) returns null we
currently drop the suggestion; instead, call the search fallback builder and use
its result. Change the NAVIGATION handling in the block using
buildNavSuggestMatch to: if navMatch is null, call the search fallback builder
(e.g., buildSearchSuggestMatch or the existing search-match helper used
elsewhere) with the same suggestion and, if that returns a match, push it and
increment count; apply the identical fallback behavior to the second NAVIGATION
handling block around the buildNavSuggestMatch call at the later range so
malformed NAVIGATION suggestions are recovered as search matches.

In `@src/renderer/src/lib/omnibox/providers/shortcut.ts`:
- Around line 117-168: The promise returned by searchShortcuts is missing
rejection handling, causing unhandled rejections and leaving onResults uncalled
on IPC errors; add a .catch handler (or use try/catch with await) around the
searchShortcuts call in the autocomplete flow so any error is caught, log or
report the error via the existing logger, and call onResults([]) (or onResults
with an empty array) and return; update the block that currently starts with
searchShortcuts(inputText, MAX_RESULTS * 2).then(...) to ensure failures call
onResults([]) and respect signal.aborted.

In `@src/renderer/src/lib/omnibox/tokenizer.ts`:
- Around line 17-35: The tokenize function's comment falsely claims it splits
camelCase but lowercases the input first, removing case boundaries; either
update the docstring/comments of tokenize to say it only splits on
non-alphanumeric characters (and recommend using tokenizeWithCamelCase for
camel-case-aware tokenization), or change tokenize to perform camelCase-aware
splitting by operating on the original input to detect camel boundaries (e.g.,
split on case transitions and non-alphanumerics) and then lowercase resulting
tokens; locate and modify the tokenize function (and/or call
tokenizeWithCamelCase) to apply one of these two fixes so the implementation
matches the documentation.

In `@src/renderer/src/lib/omnibox/url-normalizer.ts`:
- Around line 20-58: The normalizeUrlForDedup function currently uses new
URL(url) which can throw in Electron; replace that with URL.parse(url) (or the
project's URL.parse wrapper) and handle the null-return case instead of relying
on try/catch: call URL.parse(url) into a variable (e.g., parsed), return
url.toLowerCase() if parsed is null, and then adapt uses of parsed.hostname,
parsed.port, parsed.pathname, parsed.search, and parsed.hash to the URL.parse
shape; keep the rest of the normalization logic (host lowercase and strip www.,
portSuffix, path trimming, normalizePercentEncoding call, sorting params via
URLSearchParams) the same but sourced from the parsed object.

In `@src/renderer/src/routes/omnibox-debug/page.tsx`:
- Around line 416-448: The UI reads selectedSuggestion.scoringSignals.* fields
directly and can throw if scoringSignals or individual fields are undefined;
update the rendering in page.tsx to defensively access these values (e.g., use
optional chaining and sensible defaults) for
selectedSuggestion.scoringSignals.frecency, .matchQualityScore, .visitCount,
.typedCount, .urlLength and .elapsedTimeSinceLastVisit so the components render
fallback values like "N/A" or 0 instead of crashing when a provider omits a
signal; ensure the conditional that formats elapsedTimeSinceLastVisit also
handles undefined.

---

Outside diff comments:
In `@src/renderer/src/routes/omnibox-debug/page.tsx`:
- Around line 273-275: The displayed message is using a plain string so the
input variable isn't interpolated; update the JSX in the omnibox-debug page
where the Search icon and message are rendered (the block containing Search and
the text 'No suggestions found for "{input}"') to interpolate the input variable
— e.g., replace the quoted literal with a JSX expression using a template
literal or string concatenation (use `\`No suggestions found for "${input}"\``
or `'No suggestions found for "' + input + '"'`) so the actual value of the
input variable is shown.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: acbb7419-5f0e-436f-8a8d-7476b4911297

📥 Commits

Reviewing files that changed from the base of the PR and between 06635b3 and 59b6b5a.

📒 Files selected for processing (40)
  • design/omnibox-redesign.md
  • drizzle/0001_tough_scream.sql
  • drizzle/0003_omnibox_shortcuts.sql
  • drizzle/meta/_journal.json
  • src/main/controllers/app-menu-controller/menu/items/file.ts
  • src/main/controllers/windows-controller/utils/browser/omnibox.ts
  • src/main/ipc/app/new-tab.ts
  • src/main/ipc/data/omnibox-shortcuts.ts
  • src/main/ipc/index.ts
  • src/main/ipc/window/omnibox.ts
  • src/main/saving/db/schema.ts
  • src/main/saving/history/browsing-history.ts
  • src/main/saving/omnibox-shortcuts/shortcuts-service.ts
  • src/preload/index.ts
  • src/renderer/src/components/omnibox/main.tsx
  • src/renderer/src/lib/omnibox/autocomplete-controller.ts
  • src/renderer/src/lib/omnibox/autocomplete-result.ts
  • src/renderer/src/lib/omnibox/data-providers/bookmarks.ts
  • src/renderer/src/lib/omnibox/data-providers/history.ts
  • src/renderer/src/lib/omnibox/data-providers/shortcuts.ts
  • src/renderer/src/lib/omnibox/frecency.ts
  • src/renderer/src/lib/omnibox/in-memory-url-index.ts
  • src/renderer/src/lib/omnibox/omnibox.ts
  • src/renderer/src/lib/omnibox/providers/bookmark.ts
  • src/renderer/src/lib/omnibox/providers/history-quick.ts
  • src/renderer/src/lib/omnibox/providers/history-url.ts
  • src/renderer/src/lib/omnibox/providers/open-tab.ts
  • src/renderer/src/lib/omnibox/providers/pedal.ts
  • src/renderer/src/lib/omnibox/providers/search.ts
  • src/renderer/src/lib/omnibox/providers/shortcut.ts
  • src/renderer/src/lib/omnibox/providers/zero-suggest.ts
  • src/renderer/src/lib/omnibox/scoring-engine.ts
  • src/renderer/src/lib/omnibox/tokenizer.ts
  • src/renderer/src/lib/omnibox/types.ts
  • src/renderer/src/lib/omnibox/url-normalizer.ts
  • src/renderer/src/lib/search.ts
  • src/renderer/src/routes/omnibox-debug/page.tsx
  • src/shared/flow/flow.ts
  • src/shared/flow/interfaces/browser/omnibox-shortcuts.ts
  • src/shared/flow/interfaces/browser/omnibox.ts

Comment on lines +38 to +74
```
User Input
|
v
+-------------------+
| InputClassifier | URL vs Search vs Keyword
+-------------------+
|
v
+-------------------+
| AutocompleteController |
+-------------------+
/ | | | \
v v v v v
+------+ +-----+ +----+ +-----+ +--------+
| HQP | | HUP | | SP | | OTP | | Others |
+------+ +-----+ +----+ +-----+ +--------+
History History Search OpenTab Bookmarks,
Quick URL Provider Shortcuts,
Provider Provider Pedals,
\ | | | / ZeroSuggest
v v v v v
+-------------------+
| AutocompleteResult | Merge, dedupe, sort
+-------------------+
|
v
+-------------------+
| ScoringEngine | Re-rank with combined signals
+-------------------+
|
v
+-------------------+
| UI (Dropdown + |
| Inline Complete) |
+-------------------+
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add language specifiers to fenced code blocks.

Several code blocks in the design doc lack language specifiers, which affects syntax highlighting and markdown linting. The ASCII art diagram blocks should use a plaintext identifier.

📝 Example fix for the architecture diagram
-```
+```text
                           User Input
                               |
                               v

Similarly, apply language specifiers to the pseudocode blocks at lines 179, 202, 257, 273, 600, 993, and 1023 (e.g., text or plaintext for pseudocode/file lists).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
User Input
|
v
+-------------------+
| InputClassifier | URL vs Search vs Keyword
+-------------------+
|
v
+-------------------+
| AutocompleteController |
+-------------------+
/ | | | \
v v v v v
+------+ +-----+ +----+ +-----+ +--------+
| HQP | | HUP | | SP | | OTP | | Others |
+------+ +-----+ +----+ +-----+ +--------+
History History Search OpenTab Bookmarks,
Quick URL Provider Shortcuts,
Provider Provider Pedals,
\ | | | / ZeroSuggest
v v v v v
+-------------------+
| AutocompleteResult | Merge, dedupe, sort
+-------------------+
|
v
+-------------------+
| ScoringEngine | Re-rank with combined signals
+-------------------+
|
v
+-------------------+
| UI (Dropdown + |
| Inline Complete) |
+-------------------+
```
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 38-38: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@design/omnibox-redesign.md` around lines 38 - 74, The fenced ASCII diagram
containing nodes like "User Input", "InputClassifier", "AutocompleteController",
"AutocompleteResult", "ScoringEngine", and "UI (Dropdown + Inline Complete)"
should have a language specifier (use ```text or ```plaintext) to satisfy
markdown linting; update that diagram block accordingly and apply the same fix
to all other pseudocode/file-list fenced blocks (the other blocks showing
algorithm steps or file lists) by adding an appropriate language tag such as
text or plaintext so syntax highlighting and lint checks pass.

Comment on lines +11 to +23
CREATE TABLE IF NOT EXISTS `omnibox_shortcuts` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`input_text` text NOT NULL,
`destination_url` text NOT NULL,
`destination_title` text DEFAULT '' NOT NULL,
`match_type` text NOT NULL,
`hit_count` integer DEFAULT 1 NOT NULL,
`last_access_time` integer NOT NULL
);
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS `idx_omnibox_shortcuts_input` ON `omnibox_shortcuts` (`input_text`);
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS `idx_omnibox_shortcuts_destination` ON `omnibox_shortcuts` (`destination_url`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for cleanup/deletion logic using lastAccessTime
rg -n -A3 "lastAccessTime|last_access_time" --type ts | grep -E "(delete|cleanup|prune|expire|WHERE)"

Repository: MultiboxLabs/flow-browser

Length of output: 51


🏁 Script executed:

#!/bin/bash
# Search for all references to omnibox_shortcuts (all file types)
echo "=== References to omnibox_shortcuts ===" 
rg -i "omnibox_shortcuts" -A 2 -B 2

echo ""
echo "=== References to lastAccessTime or last_access_time (all file types) ==="
rg "lastAccessTime|last_access_time" -A 2 -B 2

echo ""
echo "=== Files in drizzle directory ===" 
ls -la drizzle/ | head -20

Repository: MultiboxLabs/flow-browser

Length of output: 6963


Consider adding an index on last_access_time for cleanup and sorting operations.

The schema includes indexes on input_text and destination_url, but last_access_time is used in both a cleanup query (.where(lastAccessTime < cutoff)) that performs record expiration and an ORDER BY clause for sorting results by recency. Without an index, these operations require a full table scan.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@drizzle/0003_omnibox_shortcuts.sql` around lines 11 - 23, Add an index on
omnibox_shortcuts.last_access_time to speed cleanup and recency ordering; update
the migration to create an index such as idx_omnibox_shortcuts_last_access_time
ON `omnibox_shortcuts` (`last_access_time`) so queries using
.where(lastAccessTime < cutoff) and ORDER BY last_access_time use the index.

Comment on lines 50 to 53
} else {
omnibox.setBounds(null);
omnibox.loadInterface(null);
omnibox.sendShowEvent({ currentInput: null, openIn: "new_tab" });
omnibox.show();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Minor inconsistency in call order.

The call sequence here is setBoundssendShowEventshow, whereas src/main/ipc/app/new-tab.ts uses sendShowEventsetBoundsshow. While both work (since show() is the final trigger), consider unifying the order for maintainability.

♻️ Suggested consistent ordering
        } else {
-         omnibox.setBounds(null);
          omnibox.sendShowEvent({ currentInput: null, openIn: "new_tab" });
+         omnibox.setBounds(null);
          omnibox.show();
        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else {
omnibox.setBounds(null);
omnibox.loadInterface(null);
omnibox.sendShowEvent({ currentInput: null, openIn: "new_tab" });
omnibox.show();
} else {
omnibox.sendShowEvent({ currentInput: null, openIn: "new_tab" });
omnibox.setBounds(null);
omnibox.show();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/controllers/app-menu-controller/menu/items/file.ts` around lines 50
- 53, Reorder the omnibox call sequence to match the project convention: call
omnibox.sendShowEvent(...) before omnibox.setBounds(...), and then call
omnibox.show(); specifically update the block that currently calls
omnibox.setBounds(null); omnibox.sendShowEvent({ currentInput: null, openIn:
"new_tab" }); omnibox.show(); so it becomes sendShowEvent -> setBounds -> show
using the omnibox.sendShowEvent, omnibox.setBounds, and omnibox.show methods.

Comment on lines +157 to +160
if (!this.rendererReady) {
debugPrint("OMNIBOX", "Renderer not ready yet, queueing show event");
this.pendingShowParams = params;
return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Drop queued show payloads when an explicit hide wins the race.

A show request can sit in pendingShowParams while rendererReady is false. If the omnibox is hidden before readiness, markRendererReady() still flushes that stale payload and replays omnibox:do-show into a renderer that should stay hidden, which brings the show/hide desync back.

Also applies to: 171-178, 284-291

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/controllers/windows-controller/utils/browser/omnibox.ts` around
lines 157 - 160, The code must drop queued show payloads when a hide races
ahead: when hiding the omnibox (the method that dispatches
"omnibox:do-hide"/sets hidden state), clear pendingShowParams so stale show
requests won't be replayed; additionally, update markRendererReady() to check
the current hidden/shown state before flushing pendingShowParams and only emit
"omnibox:do-show" if the omnibox is still expected to be visible. In short,
clear pendingShowParams in the hide handler and gate the pendingShowParams flush
in markRendererReady() by checking the same visibility flag used for hide/show
to avoid replaying stale payloads.

Comment on lines +248 to +254
if (suggestion.type === "NAVIGATION") {
// NavSuggest: server says this is a URL, not a query
const navMatch = this.buildNavSuggestMatch(suggestion);
if (navMatch) {
results.push(navMatch);
count++;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Actually fall back to a search match for invalid NAVIGATION suggestions.

The comments say malformed NAVIGATION suggestions should fall back to search, but buildNavSuggestMatch() returns null and the caller just drops the entry. That turns a recoverable misclassification into a lost suggestion.

💡 Suggested change
      if (suggestion.type === "NAVIGATION") {
        // NavSuggest: server says this is a URL, not a query
        const navMatch = this.buildNavSuggestMatch(suggestion);
        if (navMatch) {
          results.push(navMatch);
          count++;
+       } else {
+         results.push(this.buildSearchQueryMatch({ ...suggestion, type: "QUERY" }));
+         count++;
        }
      } else {

Also applies to: 273-285

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/lib/omnibox/providers/search.ts` around lines 248 - 254,
When buildNavSuggestMatch(suggestion) returns null we currently drop the
suggestion; instead, call the search fallback builder and use its result. Change
the NAVIGATION handling in the block using buildNavSuggestMatch to: if navMatch
is null, call the search fallback builder (e.g., buildSearchSuggestMatch or the
existing search-match helper used elsewhere) with the same suggestion and, if
that returns a match, push it and increment count; apply the identical fallback
behavior to the second NAVIGATION handling block around the buildNavSuggestMatch
call at the later range so malformed NAVIGATION suggestions are recovered as
search matches.

Comment on lines +117 to +168
// Async IPC call to search shortcuts
searchShortcuts(inputText, MAX_RESULTS * 2).then((entries) => {
if (signal.aborted) return;

if (entries.length === 0) {
onResults([]);
return;
}

// Score and rank
const scored = entries.map((entry) => ({
entry,
relevance: computeShortcutRelevance(entry, inputText.length)
}));

scored.sort((a, b) => b.relevance - a.relevance);
const top = scored.slice(0, MAX_RESULTS);

// Build matches
const matches: AutocompleteMatch[] = top.map(({ entry, relevance }) => {
const inlineCompletion = input.preventInlineAutocomplete
? undefined
: computeInlineCompletion(inputText, entry.destinationUrl);

return {
providerName: this.name,
relevance,
contents: entry.destinationTitle || entry.destinationUrl,
description: entry.destinationUrl,
destinationUrl: entry.destinationUrl,
type: "shortcut" as const,
inlineCompletion: relevance >= INLINE_COMPLETION_THRESHOLD ? inlineCompletion : undefined,
allowedToBeDefault: relevance >= INLINE_COMPLETION_THRESHOLD,
dedupKey: normalizeUrlForDedup(entry.destinationUrl),
scoringSignals: {
typedCount: 0,
visitCount: entry.hitCount,
elapsedTimeSinceLastVisit: Date.now() - entry.lastAccessTime,
frecency: 0,
matchQualityScore: 1, // Exact match by definition
hostMatchAtWordBoundary: true,
hasNonSchemeWwwMatch: true,
isHostOnly: false,
isBookmarked: false, // TODO: Cross-ref with bookmarks when available
hasOpenTabMatch: false,
urlLength: entry.destinationUrl.length
}
};
});

onResults(matches);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing error handling for shortcut search IPC.

If searchShortcuts rejects (e.g., IPC failure), the promise goes unhandled and onResults is never called.

Proposed fix
     searchShortcuts(inputText, MAX_RESULTS * 2).then((entries) => {
       if (signal.aborted) return;
       // ... existing logic ...
       onResults(matches);
-    });
+    }).catch(() => {
+      if (!signal.aborted) onResults([]);
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/lib/omnibox/providers/shortcut.ts` around lines 117 - 168,
The promise returned by searchShortcuts is missing rejection handling, causing
unhandled rejections and leaving onResults uncalled on IPC errors; add a .catch
handler (or use try/catch with await) around the searchShortcuts call in the
autocomplete flow so any error is caught, log or report the error via the
existing logger, and call onResults([]) (or onResults with an empty array) and
return; update the block that currently starts with searchShortcuts(inputText,
MAX_RESULTS * 2).then(...) to ensure failures call onResults([]) and respect
signal.aborted.

Comment on lines +17 to +35
export function tokenize(input: string): string[] {
if (!input) return [];

// First split on non-alphanumeric characters
const parts = input.toLowerCase().split(/[^a-z0-9]+/);

const tokens: string[] = [];
for (const part of parts) {
if (part.length === 0) continue;

// Split camelCase: insert boundary before uppercase letters
// Since we already lowercased, we need to do this on the original
// Actually, since we lowercased first, camelCase boundaries are gone.
// Let's handle this differently: split on the original, then lowercase.
tokens.push(part);
}

return tokens;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Misleading docstring: tokenize does not split camelCase.

The docstring claims the function splits on "camelCase boundaries," but the implementation lowercases the input first (line 21), which eliminates case information before any camelCase detection could occur. The comment at lines 27-30 acknowledges this limitation.

Consider updating the docstring to accurately reflect the behavior, or use tokenizeWithCamelCase which correctly handles camelCase splitting.

📝 Proposed docstring fix
 /**
- * Tokenize a string by splitting on non-alphanumeric characters and camelCase boundaries.
+ * Tokenize a string by splitting on non-alphanumeric characters.
  * All tokens are lowercased.
+ *
+ * Note: This does NOT split camelCase boundaries. Use tokenizeWithCamelCase()
+ * for finer granularity that preserves camelCase splits.
  *
  * Examples:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/lib/omnibox/tokenizer.ts` around lines 17 - 35, The tokenize
function's comment falsely claims it splits camelCase but lowercases the input
first, removing case boundaries; either update the docstring/comments of
tokenize to say it only splits on non-alphanumeric characters (and recommend
using tokenizeWithCamelCase for camel-case-aware tokenization), or change
tokenize to perform camelCase-aware splitting by operating on the original input
to detect camel boundaries (e.g., split on case transitions and
non-alphanumerics) and then lowercase resulting tokens; locate and modify the
tokenize function (and/or call tokenizeWithCamelCase) to apply one of these two
fixes so the implementation matches the documentation.

Comment on lines +20 to +58
export function normalizeUrlForDedup(url: string): string {
try {
const parsed = new URL(url);

// Normalize host (lowercase, remove www.)
let host = parsed.hostname.toLowerCase();
if (host.startsWith("www.")) host = host.slice(4);

// new URL() already strips scheme-default ports, so any remaining port is significant.
const port = parsed.port;
const hasNonDefaultPort = port !== "";
const portSuffix = hasNonDefaultPort ? `:${port}` : "";

// Normalize path: remove trailing slash (for all paths, not just root)
let path = parsed.pathname;
if (path.length > 1 && path.endsWith("/")) {
path = path.slice(0, -1);
}
if (path === "/") path = "";

// Normalize percent-encoding (lowercase hex digits, decode unreserved)
path = normalizePercentEncoding(path);

// Sort query parameters for consistent comparison
const params = new URLSearchParams(parsed.search);
const sortedEntries = [...params.entries()].sort((a, b) => {
const keyCompare = a[0].localeCompare(b[0]);
if (keyCompare !== 0) return keyCompare;
return a[1].localeCompare(b[1]);
});
const sortedParams = new URLSearchParams(sortedEntries);
const queryString = sortedParams.toString();

// Ignore fragment for dedup (github.com/foo#bar == github.com/foo)
return `${host}${portSuffix}${path}${queryString ? "?" + queryString : ""}`;
} catch {
return url.toLowerCase();
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider using URL.parse for Electron compatibility.

Based on learnings from this codebase, URL.parse is preferred over new URL() because it works without errors in the Electron runtime environment. URL.parse returns null on failure instead of throwing, which simplifies error handling.

♻️ Proposed refactor using URL.parse
 export function normalizeUrlForDedup(url: string): string {
-  try {
-    const parsed = new URL(url);
+  // `@ts-ignore` - URL.parse works in Electron runtime
+  const parsed = URL.parse(url);
+  if (!parsed) {
+    return url.toLowerCase();
+  }

     // Normalize host (lowercase, remove www.)
     let host = parsed.hostname.toLowerCase();
     // ... rest of the logic unchanged ...

     return `${host}${portSuffix}${path}${queryString ? "?" + queryString : ""}`;
-  } catch {
-    return url.toLowerCase();
-  }
 }

Based on learnings: "In the Flow Browser codebase, URL.parse is preferred over new URL() for URL parsing because it works without errors in the Electron runtime environment."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/lib/omnibox/url-normalizer.ts` around lines 20 - 58, The
normalizeUrlForDedup function currently uses new URL(url) which can throw in
Electron; replace that with URL.parse(url) (or the project's URL.parse wrapper)
and handle the null-return case instead of relying on try/catch: call
URL.parse(url) into a variable (e.g., parsed), return url.toLowerCase() if
parsed is null, and then adapt uses of parsed.hostname, parsed.port,
parsed.pathname, parsed.search, and parsed.hash to the URL.parse shape; keep the
rest of the normalization logic (host lowercase and strip www., portSuffix, path
trimming, normalizePercentEncoding call, sorting params via URLSearchParams) the
same but sourced from the parsed object.

Comment on lines +416 to +448
<div className="grid grid-cols-2 gap-2">
<div className="bg-secondary/30 p-2 rounded">
<span className="text-xs text-muted-foreground">Frecency</span>
<div className="font-mono font-medium">
{selectedSuggestion.scoringSignals.frecency.toFixed(2)}
</div>
</div>
<div className="bg-secondary/30 p-2 rounded">
<span className="text-xs text-muted-foreground">Match Quality</span>
<div className="font-mono font-medium">
{selectedSuggestion.scoringSignals.matchQualityScore.toFixed(3)}
</div>
</div>
<div className="bg-secondary/30 p-2 rounded">
<span className="text-xs text-muted-foreground">Visit Count</span>
<div className="font-mono font-medium">{selectedSuggestion.scoringSignals.visitCount}</div>
</div>
<div className="bg-secondary/30 p-2 rounded">
<span className="text-xs text-muted-foreground">Typed Count</span>
<div className="font-mono font-medium">{selectedSuggestion.scoringSignals.typedCount}</div>
</div>
<div className="bg-secondary/30 p-2 rounded">
<span className="text-xs text-muted-foreground">URL Length</span>
<div className="font-mono font-medium">{selectedSuggestion.scoringSignals.urlLength}</div>
</div>
<div className="bg-secondary/30 p-2 rounded">
<span className="text-xs text-muted-foreground">Last Visit</span>
<div className="font-mono font-medium text-xs">
{selectedSuggestion.scoringSignals.elapsedTimeSinceLastVisit > 0
? `${Math.round(selectedSuggestion.scoringSignals.elapsedTimeSinceLastVisit / 60000)}m ago`
: "N/A"}
</div>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Scoring signals properties accessed without null checks.

The code assumes all scoringSignals properties exist (e.g., frecency, matchQualityScore, visitCount), but different providers may populate different subsets of these fields. Consider using optional chaining or default values to prevent runtime errors when viewing matches from providers that don't set all signals.

🛡️ Suggested defensive access
                           <div className="font-mono font-medium">
-                              {selectedSuggestion.scoringSignals.frecency.toFixed(2)}
+                              {selectedSuggestion.scoringSignals.frecency?.toFixed(2) ?? "N/A"}
                            </div>

Apply similar pattern to other signal accesses.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/routes/omnibox-debug/page.tsx` around lines 416 - 448, The
UI reads selectedSuggestion.scoringSignals.* fields directly and can throw if
scoringSignals or individual fields are undefined; update the rendering in
page.tsx to defensively access these values (e.g., use optional chaining and
sensible defaults) for selectedSuggestion.scoringSignals.frecency,
.matchQualityScore, .visitCount, .typedCount, .urlLength and
.elapsedTimeSinceLastVisit so the components render fallback values like "N/A"
or 0 instead of crashing when a provider omits a signal; ensure the conditional
that formats elapsedTimeSinceLastVisit also handles undefined.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (2)
drizzle/0003_omnibox_shortcuts.sql (1)

11-13: ⚠️ Potential issue | 🟠 Major

Add recency index for cleanup/order queries (still missing).

Line 11–13 add useful indexes, but last_access_time is still unindexed. If cleanup and recency sort paths use that column, this remains a full-scan risk at scale.

Proposed migration addition
 CREATE INDEX IF NOT EXISTS `idx_omnibox_shortcuts_input` ON `omnibox_shortcuts` (`input_text`);
 --> statement-breakpoint
 CREATE INDEX IF NOT EXISTS `idx_omnibox_shortcuts_destination` ON `omnibox_shortcuts` (`destination_url`);
+--> statement-breakpoint
+CREATE INDEX IF NOT EXISTS `idx_omnibox_shortcuts_last_access_time` ON `omnibox_shortcuts` (`last_access_time`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@drizzle/0003_omnibox_shortcuts.sql` around lines 11 - 13, Add an index on the
last_access_time column to avoid full-table scans for cleanup/recency queries:
in the migration that creates indexes for table omnibox_shortcuts (see existing
index names idx_omnibox_shortcuts_input and idx_omnibox_shortcuts_destination
and the statement-breakpoint marker), add a CREATE INDEX IF NOT EXISTS for
last_access_time (e.g., idx_omnibox_shortcuts_last_access_time) so cleanup and
recency ORDER BY/WHERE on last_access_time use the index.
src/renderer/src/lib/omnibox/in-memory-url-index.ts (1)

160-170: ⚠️ Potential issue | 🟠 Major

Enforce MAX_ENTRIES on incremental inserts.

rebuild() and restoreFromCache() honor the 2k cap, but addOrUpdate() still inserts every new entry.id. After enough visits, the in-memory index and the serialized snapshot can grow without bound. Apply the same eviction/skip policy here before set() when the ID is new.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/lib/omnibox/in-memory-url-index.ts` around lines 160 - 170,
addOrUpdate currently always inserts new IDs and can grow past MAX_ENTRIES;
before calling this.entries.set(...) when existing is falsy, enforce the same
cap used by rebuild()/restoreFromCache(): if this.entries.size >= MAX_ENTRIES
evict the oldest entry (get the first key from this.entries, call
this.removeFromIndexes(oldEntry) and this.entries.delete(oldId)) or otherwise
apply the same skip policy used elsewhere, then proceed to buildEntry and
this.addToIndexes; make changes inside addOrUpdate near the
existing/removeFromIndexes/buildEntry/addToIndexes calls so behavior matches
rebuild()/restoreFromCache().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/renderer/src/lib/omnibox/in-memory-url-index.ts`:
- Around line 240-250: saveToCache currently writes raw URL/title history
(IMUISnapshot via IMUI_CACHE_KEY) into renderer localStorage; instead stop
persisting raw entries in the renderer: either send the snapshot to the
privileged main process over IPC (e.g., emit a "save-imui-cache" message from
saveToCache) so the main process stores it in a secure location, or change
saveToCache to only persist non-reconstructable metadata
(counts/timestamps/hashes) and never full URLs/titles; also ensure the cache TTL
is enforced eagerly on save (drop stale data before persisting) and update the
complementary loadFromCache logic to read from the new main-process store or to
expect the reduced metadata shape.
- Around line 127-146: populate() can be called concurrently causing parallel
fetchAndRebuild() runs that race and can overwrite fresher data; serialize full
refreshes by introducing an in-flight promise (e.g. a private _populatePromise:
Promise<void> | null) and use it to deduplicate concurrent calls: if
_populatePromise exists return/await it, otherwise set _populatePromise to an
async wrapper that performs the existing populate logic (including the
cache-restore branch and calling fetchAndRebuild(now)), and clear
_populatePromise in a finally block; ensure fetchAndRebuild uses the supplied
now and that lastRefreshTime/_populated are only set by the single serialized
run. Apply the same in-flight-promise pattern for the other populate call site
referenced (around the 310-320 logic) so all full refreshes are serialized.
- Around line 333-339: rebuild() currently slices the raw upstream entries
before converting/ranking, so slice(0, InMemoryURLIndex.MAX_ENTRIES) keeps
arbitrary first arrivals; instead map all upstream entries through buildEntry to
produce imui entries, sort those imui entries by their significance/frecency
field (the score or comparable property produced by buildEntry) in descending
order, then take the top InMemoryURLIndex.MAX_ENTRIES and call
this.entries.set(...) and addToIndexes(...) for each of those capped, preserving
existing calls to buildEntry and addToIndexes.

---

Duplicate comments:
In `@drizzle/0003_omnibox_shortcuts.sql`:
- Around line 11-13: Add an index on the last_access_time column to avoid
full-table scans for cleanup/recency queries: in the migration that creates
indexes for table omnibox_shortcuts (see existing index names
idx_omnibox_shortcuts_input and idx_omnibox_shortcuts_destination and the
statement-breakpoint marker), add a CREATE INDEX IF NOT EXISTS for
last_access_time (e.g., idx_omnibox_shortcuts_last_access_time) so cleanup and
recency ORDER BY/WHERE on last_access_time use the index.

In `@src/renderer/src/lib/omnibox/in-memory-url-index.ts`:
- Around line 160-170: addOrUpdate currently always inserts new IDs and can grow
past MAX_ENTRIES; before calling this.entries.set(...) when existing is falsy,
enforce the same cap used by rebuild()/restoreFromCache(): if this.entries.size
>= MAX_ENTRIES evict the oldest entry (get the first key from this.entries, call
this.removeFromIndexes(oldEntry) and this.entries.delete(oldId)) or otherwise
apply the same skip policy used elsewhere, then proceed to buildEntry and
this.addToIndexes; make changes inside addOrUpdate near the
existing/removeFromIndexes/buildEntry/addToIndexes calls so behavior matches
rebuild()/restoreFromCache().

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4a3bda50-7d32-4189-ab86-149ef17fbda7

📥 Commits

Reviewing files that changed from the base of the PR and between 59b6b5a and 91f2188.

📒 Files selected for processing (2)
  • drizzle/0003_omnibox_shortcuts.sql
  • src/renderer/src/lib/omnibox/in-memory-url-index.ts

Comment on lines +127 to +146
async populate(): Promise<void> {
const now = Date.now();
if (this._populated && now - this.lastRefreshTime < InMemoryURLIndex.REFRESH_INTERVAL) {
return; // Too soon for a refresh
}

// On first population, try restoring from cache for instant results
if (!this._populated) {
const restored = this.restoreFromCache();
if (restored) {
console.log(`[IMUI] Restored ${this.entries.size} entries from cache`);
// Still fetch fresh data in the background
this.fetchAndRebuild(now).catch((err) => {
console.error("[IMUI] Background refresh after cache restore failed:", err);
});
return;
}
}

await this.fetchAndRebuild(now);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Serialize full refreshes behind an in-flight promise.

populate() is kicked off during construction and again on focus, but _populated only flips after fetchAndRebuild() resolves. Two early calls can therefore issue parallel getSignificantHistory() IPCs and race to rebuild/save the cache; if the older request finishes last, it can overwrite fresher data and move lastRefreshTime backwards.

Also applies to: 310-320

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/lib/omnibox/in-memory-url-index.ts` around lines 127 - 146,
populate() can be called concurrently causing parallel fetchAndRebuild() runs
that race and can overwrite fresher data; serialize full refreshes by
introducing an in-flight promise (e.g. a private _populatePromise: Promise<void>
| null) and use it to deduplicate concurrent calls: if _populatePromise exists
return/await it, otherwise set _populatePromise to an async wrapper that
performs the existing populate logic (including the cache-restore branch and
calling fetchAndRebuild(now)), and clear _populatePromise in a finally block;
ensure fetchAndRebuild uses the supplied now and that lastRefreshTime/_populated
are only set by the single serialized run. Apply the same in-flight-promise
pattern for the other populate call site referenced (around the 310-320 logic)
so all full refreshes are serialized.

Comment on lines +240 to +250
saveToCache(): void {
if (!this._populated || this.entries.size === 0) return;

try {
const snapshot: IMUISnapshot = {
version: IMUI_CACHE_VERSION,
timestamp: Date.now(),
entries: Array.from(this.entries.values())
};
localStorage.setItem(IMUI_CACHE_KEY, JSON.stringify(snapshot));
console.log(`[IMUI] Saved ${snapshot.entries.length} entries to cache`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t persist raw history snapshots in renderer localStorage.

This stores URLs and titles in a renderer-accessible, on-disk cache, which broadens exposure of sensitive browsing history. The one-hour TTL is only enforced lazily on the next read, so the data can sit at rest much longer. If startup caching is required, keep it in a privileged main-process store or persist only non-reconstructable metadata.

Also applies to: 260-299

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/lib/omnibox/in-memory-url-index.ts` around lines 240 - 250,
saveToCache currently writes raw URL/title history (IMUISnapshot via
IMUI_CACHE_KEY) into renderer localStorage; instead stop persisting raw entries
in the renderer: either send the snapshot to the privileged main process over
IPC (e.g., emit a "save-imui-cache" message from saveToCache) so the main
process stores it in a secure location, or change saveToCache to only persist
non-reconstructable metadata (counts/timestamps/hashes) and never full
URLs/titles; also ensure the cache TTL is enforced eagerly on save (drop stale
data before persisting) and update the complementary loadFromCache logic to read
from the new main-process store or to expect the reduced metadata shape.

Comment on lines +333 to +339
// Take the most significant entries up to the cap
const capped = entries.slice(0, InMemoryURLIndex.MAX_ENTRIES);

for (const entry of capped) {
const imuiEntry = this.buildEntry(entry);
this.entries.set(entry.id, imuiEntry);
this.addToIndexes(imuiEntry);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Cap after ranking, not by upstream order.

rebuild() says it keeps the “most significant” entries, but src/renderer/src/lib/omnibox/data-providers/history.ts:17-31 only filters; it does not rank. slice(0, MAX_ENTRIES) therefore keeps whichever 2k entries arrive first instead of the highest-frecency candidates once history grows past the cap.

♻️ Suggested fix
-    // Take the most significant entries up to the cap
-    const capped = entries.slice(0, InMemoryURLIndex.MAX_ENTRIES);
+    const capped = entries
+      .map((entry) => ({
+        entry,
+        frecency: calculateFrecency(entry.visitCount, entry.typedCount, entry.lastVisitTime)
+      }))
+      .sort((a, b) => b.frecency - a.frecency || b.entry.lastVisitTime - a.entry.lastVisitTime)
+      .slice(0, InMemoryURLIndex.MAX_ENTRIES);
 
-    for (const entry of capped) {
+    for (const { entry } of capped) {
       const imuiEntry = this.buildEntry(entry);
       this.entries.set(entry.id, imuiEntry);
       this.addToIndexes(imuiEntry);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/lib/omnibox/in-memory-url-index.ts` around lines 333 - 339,
rebuild() currently slices the raw upstream entries before converting/ranking,
so slice(0, InMemoryURLIndex.MAX_ENTRIES) keeps arbitrary first arrivals;
instead map all upstream entries through buildEntry to produce imui entries,
sort those imui entries by their significance/frecency field (the score or
comparable property produced by buildEntry) in descending order, then take the
top InMemoryURLIndex.MAX_ENTRIES and call this.entries.set(...) and
addToIndexes(...) for each of those capped, preserving existing calls to
buildEntry and addToIndexes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant