Building an AI Newsletter Generator from Browser Tabs
I read a lot. Too many tabs, too many articles, too many threads — and at the end of the day I’ve skimmed a bunch of interesting things I’ve mostly forgotten or never read fully. I wanted a way to synthesize it all into something I could actually read (and maybe share). So I built a newsletter generator that reads my open browser tabs and writes it for me. It’s a trinket, a toy, something to allow me to continue my exploration of the world of AI.
It uses either Claude or Ollama with tool use, runs four agents, and streams results live to a web UI. Here’s how it works.
The Core Idea
Every time I want to generate a newsletter, I grab any links or Chrome tabs that are open in my main browser — articles I’ve read, threads I’ve followed, docs I’ve referenced - The things I want the newsletter to be about. I open them in an instance of Chrome running in Debug mode and the app treats those tabs as the raw material.
The workflow:
- Discovery phase — fetch all the open tabs, identify themes, cluster related content
- Research phase — go deeper on each cluster, pull downstream links, write the newsletter
The phases are separate by design. If I want to regenerate the writing with a different tone, I can re-run phase 2 without re-fetching everything. If Chrome crashes mid-way, phase 1 output is cached to disk.
The Four Agents
The pipeline has four agents. The first two are the main event; the other two are support roles.
Elicitor runs before anything else. It reads the open tab list and asks 2–3 focused questions: what were you trying to learn, who’s the audience, what should be grouped together? The answers get synthesized into a context block that the downstream agents use to make better decisions. If you’ve already left notes in a prompt file, it skips the questions entirely.
Discovery is phase 1. It iterates through all the open tabs, fetches each one, and groups related content into 3–8 thematic clusters. It has two tools: fetch_page(url) and submit_clusters(). When it’s satisfied it’s covered everything, it calls submit_clusters() and terminates. The clusters are cached — if you want to re-run the writing with a different tone, you can skip this phase entirely.
Research is phase 2. It takes the clusters, fetches deeper links, runs supplemental web searches, and writes the newsletter. Tools: fetch_page(url), web_search(query), and write_newsletter(). The system prompt is strict: every claim needs a source, every source needs a date. Undated material gets relegated to “additional context” sections. It won’t say “AI is changing everything.”
Podcast is the last step. It converts the finished newsletter into a spoken-word script — narrative prose, natural transitions between stories, no links, no bullet symbols. It runs on a faster model since the quality bar is different: you want it to sound good read aloud, not be a faithful rendering of every citation.
One thing that helped across the board: prefixing fetched page content with [Published: January 15, 2025] before handing it to the agent. The AI has something concrete to cite, and the prompts instruct it to include dates inline. It makes the newsletter feel grounded rather than timeless in a vague way.
Prompt Caching
The discovery and research agents both have long system prompts — detailed instructions about formatting, citation style, what to avoid, etc. I mark these with cache_control: { type: 'ephemeral' } in the Claude API calls. On the second and third runs (tweaking things, re-running phase 2), the cache hit rates are significant and the cost savings are noticeable.
I also log a full cost breakdown per run: input tokens, output tokens, cache writes, cache reads. It shows up in the live stream so I can see what each tool call is costing.
LLM Abstraction
I wanted the option to run locally without paying API costs if I don’t mind waiting. So lib/llm.js exposes a single chat() interface that routes to either Claude or Ollama depending on a LLM_PROVIDER env var. The agent code never touches the SDK directly.
I’ve tried it with both Claude and Ollama and they both produce quality output. If I’m in a hurry I’ll use Claude. If I don’t mind waiting a few hours, I’ll run it against qwen3.6:35B.
Real-Time Streaming
The server exposes a /api/stream endpoint that emits Server-Sent Events as the pipeline runs. The client sees live updates: which URLs are being fetched, when clusters are formed, thinking excerpts, tool call results, cost ticks. It makes the process feel transparent rather than like a black box.
The SSE approach was simpler than WebSockets and fit the unidirectional nature of the pipeline. There’s no back-channel needed — the user just watches it run.
Output Formats
The newsletter is stored as JSON: title, intro, an array of sections (one per cluster), closing paragraph, further reading links. A separate htmlRenderer.js converts that to styled HTML with print-optimized CSS. From there, headless Chrome generates a PDF. The podcast agent produces a plain-text spoken-word script as a fourth output.
What I’d Do Differently
A few things I’d change if I were to redo this:
Ditch the Chrome debug instance. Right now the discovery agent fetches the info from chrome tabs. That’s an artifact of what I was doing when this newsletter thing occurred to me. It’s un-necessary and it would probably be easier to just dump a list of URLs at the start of the workflow.
Richer source metadata. The fetch_page tool returns truncated plain text. A version that also extracts structured metadata — author, canonical URL, publication name, word count — would let the agent make smarter decisions about what to cite and how.
Interactive editing. A post-generation loop where you could ask for a section to be rewritten or expanded would be useful. The elicitor handles pre-run context well; there’s no equivalent for post-run refinement.
The newsletter quality is good enough that I find it useful. And since it’s just reading my open tabs, it’s capturing exactly what I was already interested in — it just does the synthesis I was too lazy to do myself.
The code is in my newsletter repo if you want to dig through it.