-
Notifications
You must be signed in to change notification settings - Fork 56
feat: ship the omnibox refactor #234
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
efde961
0cd16a7
c785534
e62f5ac
9e42c58
05cd94d
87de30d
e30c968
59b6b5a
91f2188
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| 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`); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -49,7 +49,7 @@ export const createFileMenu = (): MenuItemConstructorOptions => ({ | |||||||||||||||||||
| omnibox.hide(); | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| omnibox.setBounds(null); | ||||||||||||||||||||
| omnibox.loadInterface(null); | ||||||||||||||||||||
| omnibox.sendShowEvent({ currentInput: null, openIn: "new_tab" }); | ||||||||||||||||||||
| omnibox.show(); | ||||||||||||||||||||
|
Comment on lines
50
to
53
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Minor inconsistency in call order. The call sequence here is ♻️ Suggested consistent ordering } else {
- omnibox.setBounds(null);
omnibox.sendShowEvent({ currentInput: null, openIn: "new_tab" });
+ omnibox.setBounds(null);
omnibox.show();
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,12 @@ const omniboxes = new Map<BrowserWindow, Omnibox>(); | |
|
|
||
| type QueryParams = { [key: string]: string }; | ||
|
|
||
| /** Parameters sent to the renderer when showing the omnibox. */ | ||
| interface OmniboxShowParams { | ||
| currentInput: string | null; | ||
| openIn: "current" | "new_tab"; | ||
| } | ||
|
|
||
| export class Omnibox { | ||
| public view: WebContentsView; | ||
| public webContents: WebContents; | ||
|
|
@@ -17,6 +23,13 @@ export class Omnibox { | |
|
|
||
| private isDestroyed: boolean = false; | ||
|
|
||
| /** Whether the initial load of the omnibox URL has completed. */ | ||
| private initialLoadComplete: boolean = false; | ||
| /** Whether the renderer has registered its IPC listeners and can receive show events. */ | ||
| private rendererReady: boolean = false; | ||
| /** Most recent show payload queued while waiting for renderer readiness. */ | ||
| private pendingShowParams: OmniboxShowParams | null = null; | ||
|
|
||
| constructor(parentWindow: BrowserWindow) { | ||
| debugPrint("OMNIBOX", `Creating new omnibox for window ${parentWindow.id}`); | ||
| const onmiboxView = new WebContentsView({ | ||
|
|
@@ -46,6 +59,18 @@ export class Omnibox { | |
| this.updateBounds(); | ||
| }); | ||
|
|
||
| onmiboxWC.on("did-start-loading", () => { | ||
| this.rendererReady = false; | ||
| }); | ||
|
|
||
| // Track when the initial load finishes | ||
| onmiboxWC.on("did-finish-load", () => { | ||
| if (!this.initialLoadComplete) { | ||
| this.initialLoadComplete = true; | ||
| debugPrint("OMNIBOX", "Initial load complete"); | ||
| } | ||
| }); | ||
|
|
||
| setTimeout(() => { | ||
| this.loadInterface(null); | ||
| this.updateBounds(); | ||
|
|
@@ -65,9 +90,33 @@ export class Omnibox { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Load the omnibox interface URL. Only used for initial load. | ||
| * After the first load, the renderer stays alive and receives IPC messages. | ||
| */ | ||
| loadInterface(params: QueryParams | null) { | ||
| this.assertNotDestroyed(); | ||
|
|
||
| // If the omnibox renderer is already loaded, send params via IPC instead of reloading | ||
| if (this.initialLoadComplete) { | ||
| if (!this.rendererReady) { | ||
| debugPrint("OMNIBOX", "Renderer not ready yet, queueing show event until listener registration"); | ||
| this.pendingShowParams = { | ||
| currentInput: params?.currentInput ?? null, | ||
| openIn: (params?.openIn as "current" | "new_tab") ?? "new_tab" | ||
| }; | ||
| return; | ||
| } | ||
|
|
||
| debugPrint("OMNIBOX", "Omnibox already loaded, sending show event via IPC instead of reloading"); | ||
| const showParams: OmniboxShowParams = { | ||
| currentInput: params?.currentInput ?? null, | ||
| openIn: (params?.openIn as "current" | "new_tab") ?? "new_tab" | ||
| }; | ||
| this.webContents.send("omnibox:do-show", showParams); | ||
| return; | ||
| } | ||
|
|
||
| debugPrint("OMNIBOX", `Loading interface with params: ${JSON.stringify(params)}`); | ||
| const onmiboxWC = this.webContents; | ||
|
|
||
|
|
@@ -88,6 +137,56 @@ export class Omnibox { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Send a show event to the already-loaded omnibox renderer via IPC. | ||
| * This is the preferred method after the initial load — no reload, no flicker. | ||
| */ | ||
| sendShowEvent(params: OmniboxShowParams) { | ||
| this.assertNotDestroyed(); | ||
|
|
||
| if (!this.initialLoadComplete) { | ||
| debugPrint("OMNIBOX", "Initial load not complete, routing show event through loadInterface"); | ||
| const queryParams: QueryParams = { openIn: params.openIn }; | ||
| if (params.currentInput !== null) { | ||
| queryParams.currentInput = params.currentInput; | ||
| } | ||
| this.loadInterface(queryParams); | ||
| return; | ||
| } | ||
|
|
||
| if (!this.rendererReady) { | ||
| debugPrint("OMNIBOX", "Renderer not ready yet, queueing show event"); | ||
| this.pendingShowParams = params; | ||
| return; | ||
|
Comment on lines
+157
to
+160
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Drop queued show payloads when an explicit hide wins the race. A show request can sit in Also applies to: 171-178, 284-291 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| debugPrint("OMNIBOX", `Sending show event with params: ${JSON.stringify(params)}`); | ||
| this.webContents.send("omnibox:do-show", params); | ||
| } | ||
|
|
||
| markRendererReady() { | ||
| this.assertNotDestroyed(); | ||
| this.rendererReady = true; | ||
|
|
||
| if (!this.pendingShowParams) { | ||
| return; | ||
| } | ||
|
|
||
| const params = this.pendingShowParams; | ||
| this.pendingShowParams = null; | ||
| debugPrint("OMNIBOX", "Renderer ready, flushing queued show event"); | ||
| this.webContents.send("omnibox:do-show", params); | ||
| } | ||
|
|
||
| /** | ||
| * Send a hide event to the renderer so it can reset its state. | ||
| */ | ||
| sendHideEvent() { | ||
| this.assertNotDestroyed(); | ||
| debugPrint("OMNIBOX", "Sending hide event to renderer"); | ||
| this.webContents.send("omnibox:do-hide"); | ||
| } | ||
|
|
||
| updateBounds() { | ||
| this.assertNotDestroyed(); | ||
|
|
||
|
|
@@ -140,7 +239,7 @@ export class Omnibox { | |
| this.assertNotDestroyed(); | ||
|
|
||
| debugPrint("OMNIBOX", "Showing omnibox"); | ||
| // Hide omnibox if it is already visible | ||
| // Hide omnibox if it is already visible (safe: hide() no longer sends IPC) | ||
| this.hide(); | ||
|
|
||
| // Show UI | ||
|
|
@@ -182,6 +281,14 @@ export class Omnibox { | |
| debugPrint("OMNIBOX", "Hiding omnibox"); | ||
| this.view.setVisible(false); | ||
|
|
||
| // Do NOT send a hide IPC event here. The native view visibility | ||
| // (view.setVisible) is the sole mechanism for main-process hides. | ||
| // Sending hide IPC caused a race condition: blur events during show() | ||
| // would trigger maybeHide() → hide() → sendHideEvent(), undoing the | ||
| // prior sendShowEvent() and leaving the renderer with isVisible=false. | ||
| // The renderer handles its own state for user-initiated hides (Escape, | ||
| // match selection) and the show handler always resets state on next open. | ||
|
|
||
| if (omniboxWasFocused) { | ||
| // Focuses the parent window instead | ||
| this.window.webContents.focus(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,37 @@ | ||||||||||||||||||||||||||||||||
| import { ipcMain } from "electron"; | ||||||||||||||||||||||||||||||||
| import { omniboxShortcutsService } from "@/saving/omnibox-shortcuts/shortcuts-service"; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // --- Omnibox Shortcuts IPC Handlers --- | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||
| * Search for shortcuts matching the given input text. | ||||||||||||||||||||||||||||||||
| * Used by ShortcutsProvider in the renderer for omnibox suggestions. | ||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||
| ipcMain.handle("omnibox-shortcuts:search", async (_event, inputText: string, limit?: number) => { | ||||||||||||||||||||||||||||||||
| return omniboxShortcutsService.search(inputText, limit); | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||
| * Record a shortcut when the user selects an omnibox suggestion. | ||||||||||||||||||||||||||||||||
| * Fire-and-forget — called from the renderer when the user picks a result. | ||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||
| ipcMain.on( | ||||||||||||||||||||||||||||||||
| "omnibox-shortcuts:record-usage", | ||||||||||||||||||||||||||||||||
| (_event, inputText: string, destinationUrl: string, destinationTitle: string, matchType: string) => { | ||||||||||||||||||||||||||||||||
| omniboxShortcutsService.recordUsage(inputText, destinationUrl, destinationTitle, matchType); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
|
Comment on lines
+18
to
+23
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider basic input validation for IPC parameters. The ♻️ Suggested validation ipcMain.on(
"omnibox-shortcuts:record-usage",
(_event, inputText: string, destinationUrl: string, destinationTitle: string, matchType: string) => {
+ if (typeof inputText !== "string" || typeof destinationUrl !== "string") {
+ return; // Silently ignore malformed requests
+ }
omniboxShortcutsService.recordUsage(inputText, destinationUrl, destinationTitle, matchType);
}
);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||
| * Get all shortcuts for a specific destination URL. | ||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||
| ipcMain.handle("omnibox-shortcuts:get-for-url", async (_event, destinationUrl: string) => { | ||||||||||||||||||||||||||||||||
| return omniboxShortcutsService.getForUrl(destinationUrl); | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||
| * Cleanup old shortcuts. Returns number of rows deleted. | ||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||
| ipcMain.handle("omnibox-shortcuts:cleanup", async (_event, maxAgeDays?: number) => { | ||||||||||||||||||||||||||||||||
| return omniboxShortcutsService.cleanup(maxAgeDays); | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,7 +20,11 @@ ipcMain.on("omnibox:show", (event, bounds: Electron.Rectangle | null, params: { | |
|
|
||
| const omnibox = parentWindow.omnibox; | ||
| omnibox.setBounds(bounds); | ||
| omnibox.loadInterface(params); | ||
| // Send params via IPC instead of reloading | ||
| omnibox.sendShowEvent({ | ||
| currentInput: params?.currentInput ?? null, | ||
| openIn: (params?.openIn as "current" | "new_tab") ?? "new_tab" | ||
| }); | ||
|
Comment on lines
+23
to
+27
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Type cast is unsafe — validate or fallback defensively. The cast 🛡️ Proposed defensive validation // Send params via IPC instead of reloading
+ const openIn = params?.openIn === "current" ? "current" : "new_tab";
omnibox.sendShowEvent({
currentInput: params?.currentInput ?? null,
- openIn: (params?.openIn as "current" | "new_tab") ?? "new_tab"
+ openIn
});🤖 Prompt for AI Agents |
||
| omnibox.show(); | ||
| }); | ||
|
|
||
|
|
@@ -40,3 +44,17 @@ ipcMain.on("omnibox:hide", (event) => { | |
| const omnibox = parentWindow.omnibox; | ||
| omnibox.hide(); | ||
| }); | ||
|
|
||
| ipcMain.on("omnibox:renderer-ready", (event) => { | ||
| const omniboxWindow = browserWindowsManager | ||
| .getAll() | ||
| .find((window) => "omnibox" in window && window.omnibox.webContents === event.sender); | ||
|
|
||
| if (!omniboxWindow || !browserWindowsManager.isInstanceOf(omniboxWindow)) { | ||
| debugPrint("OMNIBOX", "Renderer-ready received for unknown omnibox webContents"); | ||
| return; | ||
| } | ||
|
|
||
| debugPrint("OMNIBOX", "Renderer reported ready for IPC show events"); | ||
| omniboxWindow.omnibox.markRendererReady(); | ||
|
Comment on lines
+48
to
+59
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial LGTM — renderer-ready handler is correctly implemented. The handler properly matches the sender webContents to identify the correct omnibox instance and calls Note: Per context snippets, 🤖 Prompt for AI Agents |
||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -123,3 +123,28 @@ export const historyVisits = sqliteTable( | |
|
|
||
| export type HistoryUrlRow = typeof historyUrls.$inferSelect; | ||
| export type HistoryVisitRow = typeof historyVisits.$inferSelect; | ||
|
|
||
| // --- Omnibox Shortcuts Table --- | ||
| // Learned input-to-destination mappings (e.g., typing "gi" → github.com). | ||
| // Separate from browsing history: this tracks what users select for specific | ||
| // omnibox input, not every page visit. | ||
|
|
||
| export const omniboxShortcuts = sqliteTable( | ||
| "omnibox_shortcuts", | ||
| { | ||
| id: integer("id").primaryKey({ autoIncrement: true }), | ||
| inputText: text("input_text").notNull(), | ||
| destinationUrl: text("destination_url").notNull(), | ||
| destinationTitle: text("destination_title").notNull().default(""), | ||
| matchType: text("match_type").notNull(), | ||
| hitCount: integer("hit_count").notNull().default(1), | ||
| lastAccessTime: integer("last_access_time").notNull() | ||
| }, | ||
| (table) => [ | ||
| index("idx_omnibox_shortcuts_input").on(table.inputText), | ||
| index("idx_omnibox_shortcuts_destination").on(table.destinationUrl) | ||
| ] | ||
| ); | ||
|
Comment on lines
+132
to
+147
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check how recordUsage handles existing shortcuts
echo "=== shortcuts-service recordUsage implementation ==="
ast-grep --pattern 'recordUsage($$$) {
$$$
}'
echo ""
echo "=== Check for upsert/conflict handling ==="
rg -n "onConflict|upsert|INSERT OR" --type ts src/main/saving/omnibox-shortcuts/Repository: MultiboxLabs/flow-browser Length of output: 165 🏁 Script executed: #!/bin/bash
# Find files related to omnibox shortcuts
echo "=== Finding omnibox/shortcuts related files ==="
find . -type f -name "*.ts" -o -name "*.tsx" | xargs grep -l "recordUsage" | head -20
echo ""
echo "=== Looking for shortcuts service ==="
fd -e ts -e tsx "shortcut" | grep -v node_modules | head -20
echo ""
echo "=== Search for omniboxShortcuts usage ==="
rg "omniboxShortcuts" --type ts -A 3 -B 3 | head -100Repository: MultiboxLabs/flow-browser Length of output: 7539 🏁 Script executed: #!/bin/bash
# Read the shortcuts-service.ts file
echo "=== shortcuts-service.ts full content ==="
cat -n src/main/saving/omnibox-shortcuts/shortcuts-service.tsRepository: MultiboxLabs/flow-browser Length of output: 5837 The schema doesn't require a unique constraint on The service checks for existing 🤖 Prompt for AI Agents |
||
|
|
||
| export type OmniboxShortcutRow = typeof omniboxShortcuts.$inferSelect; | ||
| export type OmniboxShortcutInsert = typeof omniboxShortcuts.$inferInsert; | ||
There was a problem hiding this comment.
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:
Repository: MultiboxLabs/flow-browser
Length of output: 51
🏁 Script executed:
Repository: MultiboxLabs/flow-browser
Length of output: 6963
Consider adding an index on
last_access_timefor cleanup and sorting operations.The schema includes indexes on
input_textanddestination_url, butlast_access_timeis 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