← Back to work

Smart Expansion History

A Chrome extension, History Chat, that finds a page you visited when you only remember what it was about, not its title or URL. I built it with React and Vite during my internship at DevFortress: the search runs on Chrome's local history, and Google's Gemini API is used only if you add your own key.

Architecture

Extension popup (React)Chrome historyGemini (optional)
Gemini is called only when the user adds a key, to turn the question into keywords and to rank up to 100 local candidates.

The extension is a Manifest V3 popup built with React, with no server of its own and no content scripts. Without a key, the typed text goes straight to chrome.history.search over the time range the user picks, from the last 24 hours to the last 3 months, and the matches are cleaned and listed on the spot. With a key, Gemini 2.5 Flash first turns the question into keywords ordered by relevance, the popup searches the local history keyword by keyword until it holds 100 cleaned candidates or runs out, and Gemini then picks up to 5 of them with a short reason. Settings, including the Gemini key, are kept in Chrome's synced extension storage, and the chat log in local storage.

Key decisions

Four search depths the user picks

I let the user choose how far the AI expands a question: Superficial asks Gemini for 5 keywords, Regular (the default) for 20, Exhaustive for 50, and Sheerlock for up to 100. The prompt asks for single words with no repeats, ordered from most to least relevant, and tells the model not to pad the list. A quick lookup stays small, and the deep mode is there for pages that are hard to find.

A fixed budget of 100 candidates

Whatever the depth, at most 100 pages are sent to Gemini to rank. The top 10 keywords get up to 10 slots each, the next 20 get 5, the rest get 2, and a second pass fills any free slots with the leftovers, still in keyword order. With this progressive retrieval, the extension found more of the right pages and used about 40% fewer Gemini tokens.

Clean the list before the model sees it

I normalize every candidate URL: I drop the part after the # and tracking parameters like utm_source, gclid, and fbclid, then sort the rest. Duplicates then collapse into one entry, and Google search pages, chrome:// pages, and local files are left out. So no slot in the budget goes to the same page twice or to search noise.

The hard part

When the model's JSON is not valid JSON

Both Gemini calls need raw JSON back, and both prompts ask for it, but I still strip markdown code fences before parsing and pull the bracketed array out of the keyword reply. If the keywords still cannot be read, or the call fails, the extension falls back to the words of the question longer than three characters and searches with those. If the ranking cannot be read, the user still gets the first 5 candidates instead of an error.

Security and performance

  • Without an API key, the extension makes no network calls: the search runs on Chrome's history API. With a Gemini key, the question and the title, URL, and last visit time of up to 100 candidate pages are sent to Google's Gemini API, but never the full history.
  • Apart from host access for the AI APIs, the manifest asks only for the history and storage permissions, and nothing is injected into the pages you visit. The chat log, past results included, stays in local extension storage, and messages older than the chosen retention window are deleted when the popup opens.

Stack

ReactViteTailwind CSSChrome Manifest V3Gemini API

Links