AI News HubLIVE
In-site rewrite6 min read

The classic Java RSS reader won't run in 2026, so I rebuilt it to the web

A developer rebuilt the abandoned Java desktop RSS reader RSSOwl for the web using AI (Claude Code) and Vaadin 25. Most of the UI transferred quickly, but the AI produced incorrect APIs due to outdated training data. With the help of an MCP server for current docs and manual verification against the original, a multi-user reader emerged, though some features (pluggable menus, embedded browser) were impossible to port.

SourceHacker News AIAuthor: spicyroman

Blog

I let AI rebuild a dead Java desktop app for the web. Here's where it broke.

By

Enver Haase-Beer

·

On Jul 16, 2026 11:39:14 AM

·

In Development

I came to RSS about twenty years late. When I finally went looking for a good reader, one beloved old Eclipse desktop app kept turning up on every "best of" list—RSSOwl—and it wouldn't even launch on my Mac. I'm a developer, so instead of shrugging I got nosy: how hard would it actually be to rebuild its main screen for the browser? Here's what I found, what came across in an afternoon, what fought me, and what turned out to be impossible.

I read a fair amount of developer stuff (the Vaadin blog, the Spring blog, a couple of Java ones, Hacker News), and half of it only really comes in email now. My inbox had become a graveyard of unread content. RSS promised to put it all in one place, newest first, without giving my email address to anyone – and let me actually get through it.

So I looked around for something to read it in. One name kept coming up, on nearly every "best RSS reader" list (it's still near the top of SourceForge's Java RSS readers for Mac) and in half the old forum threads: RSSOwl, a gorgeous Eclipse desktop reader with a classic three-pane layout — a tree of feeds on the left, a sortable table of headlines top-right, an article reader below. The lists tended to add the same asterisk — needs Java, looks dated — which I ignored. I downloaded it. Nothing happened. It just won't run on a 2026 Mac.

The dead binary (and the plot twist)

The official Mac build of RSSOwl won't start at all on a modern Mac. It’s an old 32-bit binary linked against Carbon, a UI layer Apple killed years ago Rosetta can't help it — it only translates 64-bit Intel code. The flagship download of the app simply cannot run on a 2026 machine. That's what "stranded on a dying desktop toolkit" actually feels like.

Then along came the maintained community fork of RSSOwlnix, distributed on GitHub, building a 64-bit Intel binary. That one does run on a current Mac, but under Rosetta, which itself is on the way out.

So I built it myself from source, and in just over two minutes I got a native Apple Silicon app that runs with no Rosetta at all because modern SWT ships a current aarch64 build. It took changing one line in the build config (a Tycho target environment from x86_64 to aarch64) to get it.

"You cannot run SWT on Apple Silicon" is simply false. The problem is not the toolkit, but the distribution.

That distinction has stuck with me and is a good reason to move something like that to the web. It's not that the desktop toolkit can't keep up — it can. The problem is that the only build a normal person can actually get is a crumbling 32-bit relic (or an Intel one riding Rosetta, itself on the way out), and getting a modern native build requires source access, a whole Maven/Tycho toolchain, the right JDK, and knowing exactly which knob to turn. No one who downloads a feed reader is going to do that. But you just opened a web app.

So, as a web developer, I couldn't help but ask myself:

Could I take that exact screen — a feeds tree, a sortable headlines table with a right-click menu, and an article reader — and have an AI rebuild it in the browser? Maybe even make it multi-user, so friends could have their own feed lists? And where could it fall apart?

So I decided to find out, using Claude Code to do the typing and keeping a tally of what worked and what didn't. I was targeting Vaadin 25 (Java, server-side UI — not a JS rewrite, that was the point). Short version: the screen is moving. Much of it moves surprisingly fast. But “where does it fall apart” has some real answers. And the most interesting answer has nothing to do with widgets.

What came across clean and fast

The headlines table is the core of RSSOwl. It looks like a plain table, but it's secretly a tree (so it can group rows), with sortable columns, bold unread rows, custom row colors, clickable in-cell icons, and a right-click menu. That's the part I didn't think would be easy to move.

Most of it came fast:

Selecting a headline updating the reader "just worked" — via Signals. The AI was trying to peddle me something stale here at first: it wired the master→detail link with old-style change listeners. Vaadin 25 has a better solution — Signals. The headline that is selected lives in a ValueSignal, and the reader binds to it reactively (Signal.effect(...)); there's no manual plumbing of "when the selection changes, go update the reader." Better than the desktop original, though, but I only found it because I checked, which is a theme below.

Sorting was almost free. You get the built-in click-to-sort headers, and the little sort arrow. Just give each column a Comparator.

The right-click menu was rendered faithfully. A GridContextMenu rebuilt on each open reproduced "Mark read" flipping to "Mark unread," and correctly shows nothing when you right-click a group header.

And then the thing that really surprised me: how much code just disappeared. Counting real lines (comments excluded):

RSSOwl's NewsComparator has gone from 306 lines to about 10 lines of Grid column comparators. Its 467-line NewsTableLabelProvider — bold unread, highlighted "sticky" rows, label colours — became some 10 lines of CSS.

All that imperative desktop ceremony — comparators, content providers, label providers, hand-drawn row painting — collapses into a bit of declarative Grid config and some CSS. (To be fair, some of the shrink is because my version does a little less than the original in the edge cases. But even accounting for that, the difference is enormous.)

Where the AI let me down

This is the part most articles skip, and it's the part I'd most like to read. Common thread: none of the issues below came from anything the AI knew — they came from the compiler, from running the app (sometimes the AI itself caught them, by driving the app with Playwright), or from me comparing against how the original behaved.

It confidently wrote APIs that don't exist, because it had to. Here's how it works, and it's the single most important thing to understand before you allow an AI to work on a modern framework: the model has a cutoff date for its training data, and Vaadin 25 was released after that date. So this API has never been seen by the model. Ask it anyway and it doesn't say "I don't know" — it pattern-matches on the older Vaadin it learned on and gives you confident, plausible, wrong code (deprecated constructors, moved methods, the pre-Signals listener style). It looks fine, but it will not compile.

Without live docs, the AI answers your Vaadin 25 questions from pre-25 (Vaadin 24-era) training data — fluently, and incorrectly. This is why you need the MCP server.

The fix is that Vaadin ships an MCP server that feeds its current docs directly to the AI. Consider the model's built-in framework knowledge as to be stale cache and make querying the MCP server for the real 25.x API to be a required step, not an afterthought. That one habit is what brought Signals, above, the current security config and the modern Upload/Download handlers into the light of day instead of fiction. Even with the right API, there are still sharp edges the AI face-planted on: a Grid row double-click event gives you the item directly, but the context-menu event gives you an Optional — the same "get the clicked row," written two different ways.

A styling trick straight from the docs silently did nothing. The docs say to use the --vaadin-grid-cell-background custom property to color a row’s background. I did. It was applied to the cell, and the background stubbornly remained white. With the current Aura theme, the cell background is painted separately, so the property is ignored. The only thing that worked was background-color: … !important for the cell's ::part(...). I only found this out by inspecting the live shadow DOM. One-line fix - and a reminder that even doc-sanctioned advice should be verified against the running app. This would have taken probably an hour to figure out - luckily, the AI caught it with Playwright.

You can't guess, a Signal's got you. Reading the selection from a background timer with signal.get() throws — get() sets up reactive dependency tracking and is illegal outside an effect. You'll have to use signal.peek() from a plain callback. Obvious in hindsight, invisible until you ran the app. And again, only by opening the app did it surface.

A sorting subtlety it got quietly wrong. RSSOwl always sorts undated items to the bottom, either direction. Vaadin gives a column one comparator and reverses it for a descending sort, so my nullsLast flipped to nullsFirst and empty-date rows leapt to the top of the default newest-first view. Wrong in a way you'd only notice if you knew the original.

But the biggest lesson was bigger than any single bug. The AI made things on its own that looked right and weren’t. It created a set of feed categories that were different from the real ones in RSSOwl. It quietly shipped fewer feeds than the original and never mentioned it. It made up an arbitrary limit for how many articles to keep — until I pointed it at RSSOwl's actual source, where the real default (200 per feed) was sitting in plain sight. Every one of those looked completely plausible.

The AI got me a faithful skeleton, in hours. It needed someone who could replicate the behaviour of the original to turn it into a faithful app — someone who, this time, was me.

That’s the one thing I would say to anyone trying this. The tooling reproduces structure amazingly fast and under-delivers completeness just as reliably. It nails the broad 80% in an afternoon; the last 20% — the fidelity that makes it really the same app and not a convincing lookalike — needs someone who remembers what the original actually did (or who would figure it out). Not as a nice-to-have. As the factor that decides whether the result is correct.

What I could just not do

Some things didn't so much fight me as slam a door. Briefly:

Eclipse's pluggable menus have no web equivalent. Other plugins can add items to a menu via Eclipse extension registry (in RCP). That's an architecture, not a widget. And there is just nothing on the web to map it onto. That’s a redesign, not a port, if an app uses that kind of extensibility

A real embedded browser is blocked by the web itself. RSSOwl has a real browser embedded with the live page for the reader Drop a news site into an today and it renders blank - sites forbid it with X-Frame-Options / CSP headers. I render the feed's own article HTML inline instead, first cleaned through a jsoup allow-list, because Vaadin's Html component doesn't sanitize, a documented footgun. Not the same thing but close in spirit.

Some features are just gone. RSSOwl syncs with Google Reader — a service that Google shut down in 2013. If the other half is gone, you can't move a feature.

The awkward rendering case is custom row colour plus the selection highlight. An arbitrary colour per-row and the selected-row state don’t compose cleanly through CSS parts, so I settled for a compromise rather than a perfect match.

None of these are really the web framework's fault. They're the real edges of dragging a 20-year-old desktop app onto today's platform. Knowing where those edges are before you start is worth more than any feature list.

A bonus I didn't count on

I just wanted to prove the main screen could move. But every time I checked "could this part move too?" — logins, full-text search, filters, labels, keyboard shortcuts — it turned into a working feature, and I ended up with the multi-user feed reader I'd half-jokingly asked for, one I'd actually use. It says a lot that the real thing sort of came out of just poking at it.. (With the caveats above firmly attached: I still had to catch every completeness gap myself, and one polished screen is not the same as porting a whole applica

[truncated for AI cost control]