Radicle Desktop

Radicle is a way to collaborate on code without a central server: your repositories, issues and patches live on your own machine and replicate peer to peer. Radicle Desktop is the native app for all of that: browsing repos, filing issues, and reviewing patches with your own node running underneath.

Role: Visual Design, Design System, Code


A tour of Radicle Desktop: the inbox, issues, patches, code review and boards.

I’ve worked at Radicle since 2020. Early on I designed Radicle’s brand, website and first app, and in 2025 I turned to the desktop app.

This time I didn’t hand anyone a Figma file. I designed directly in the code and sent my work as patches, Radicle’s version of pull requests. So the tool I was designing was also the tool I used to ship the designs, and the maintainers (mostly Rudolfs, who reviewed an absurd amount of my work) reviewed my design decisions the same way they review everyone’s code. Every annoying thing I ran into while using Radicle was something I could just go fix.

67patches opened
40merged
6open for review
21prototypes

Jump to: Design system, Code review, Issues, Sidebar, Inbox, Writing, Smart links, The network, Multi-device identity, Boards, Navigator

Design system

Radicle got a new brand and the start of a new design system, which we developed together with the studio A Color Bright. My part was taking that system from a set of design files to two shipping apps, and growing it as the apps needed more than the first version covered.

Color works in two layers. Underneath are primitive neutrals: an opaque ramp and a transparent ramp, each in a light and a dark version. On top of those sit semantic tokens with names that say what they’re for: surfaces (base, canvas, subtle), text (primary down to quaternary), borders, feedback colors, and brand. A component never asks for a hex value. It asks for “subtle surface” or “tertiary text”, and switching between light and dark mode only swaps what sits underneath.

Primitives
Light
0
50
100
150
200
250
300
400
500
600
700
800
900
Dark
0
50
100
150
200
250
300
400
500
600
700
800
900
Semantic tokenLightDark
--color-surface-baselight-50dark-0
--color-surface-canvaslight-0dark-50
--color-surface-subtlelight-100dark-100
--color-border-subtlelight-150dark-150
--color-text-tertiarylight-600dark-600
--color-text-secondarylight-700dark-700
--color-text-primarylight-900dark-900
--color-surface-brandblue-500blue-500
Components use the names on the left. Light and dark mode only change what they point to.

Type is just as small a set. The interface uses one typeface, Booton, in three weights: regular, semibold, and a medium that sits at 468 instead of the usual 500, so it reads as a step up from body text without competing with headings. There are seven heading sizes and three body sizes, and each token is a full font shorthand (size, line height and weight together), so a component picks one name and gets the whole style. Code uses JetBrains Mono, and people can switch it to their system monospace in settings.

Aa
Booton Regular · 400
Aa
Booton Medium · 468
Aa
Booton Semibold · 600
Headings and body
--txt-heading-xxl32/36 · 600Offline-first sync
--txt-heading-xl28/32 · 600Offline-first sync
--txt-heading-l22/28 · 600Offline edits lost when a peer reconnects
--txt-heading-m18/20 · 600Offline edits lost when a peer reconnects
--txt-heading-s16/20 · 600Offline edits lost when a peer reconnects
--txt-heading-xs14/20 · 600Offline edits lost when a peer reconnects
--txt-heading-xxs12/16 · 600Offline edits lost when a peer reconnects
--txt-body-l-regular16/24 · 400Queued ops are replayed after the handshake finishes.
--txt-body-m-regular14/20 · 400Queued ops are replayed after the handshake finishes.
--txt-body-m-medium14/20 · 468Queued ops are replayed after the handshake finishes.
--txt-body-m-semibold14/20 · 600Queued ops are replayed after the handshake finishes.
--txt-body-s-regular12/16 · 400Queued ops are replayed after the handshake finishes.
Code
--txt-code-regular14/20 · 400git push rad HEAD:refs/patches
Each token sets size, line height and weight at once. Sizes in pixels.

A system is also a set of rules. Buttons are a good example: there is very rarely a primary action on a screen, so a filled brand button is saved for the one thing you came to do (Review on a patch, New issue on the issue list), and everything else in the top bar is a quiet ghost button. Every variant and size lives in one place so nobody has to guess.

BrandDestructiveNeutral
PrimaryBrandDestructiveNeutral
SecondaryBrandDestructiveNeutral
OutlineBrandDestructiveNeutral
GhostBrandDestructiveNeutral
SizesSmallRegularLarge
Four variants, three intents, three sizes.

The same tokens build everything else: status chips that say where a patch or issue stands, badges, banners, form controls, menus, and an icon set drawn on a 16px grid to match.

Status
Patch
OpenDraftArchivedMerged
Issue
OpenClosed as solved
In lists
Badges
Visibility
Public12PrivateLocal
Identity
AcceptedActiveRejected
Review
addeddeletedmovedReview in progress
Neutral
bugsync+144-81646713You7@ana
Banners
Inline
There’s a problem with your COB cache, so some patches may not be displayed. You can rebuild the cache to resolve this.Rebuild cache
Markdown
Note

Useful information that users should know, even when skimming content.

Tip

Helpful advice for doing things better or more easily.

Important

Key information users need to know to achieve their goal.

Warning

Urgent info that needs immediate user attention to avoid problems.

Caution

Advises about risks or negative outcomes of certain actions.

Controls
Inputs
Search repositoriesResume interrupted handshakesrad:not-a-repo
Checkbox
Show archivedNotify meDisabled
Switches
ShowHide
Tabs
FilesCommits128
Keys
⌘+K⌘+B?
Overlays
Menu
View identity documentCopy RIDCopy checkout commandCopy link to app.radicle.xyzOpen in app.radicle.xyzUnseed
Copy commit ID
Rebuilding
Icons
Chips, banners, controls, menus and all 115 icons, drawn on a 16px grid. Every color is a token, so the toggle swaps them all.

The payoff has been bigger than I expected. Because every color has a name that says what it’s for, and every component follows the same few rules, the system is easy to build with, for people and for AI. The team builds a lot of Radicle with Claude Code, and a good design system turns out to be exactly what it needs: it picks the right token and the right button on its own. Most of the time developers can build new features with very little back and forth with me, and my job shifts from drawing every screen to reviewing and refining.

I also updated the app icon to the new brand.

The new app icon.
The new app icon.

Patches and code review

This was the big one. Patches are Radicle’s pull requests, and they are the heart of any code collaboration tool. Redesigning them took about five weeks of hands-on work, roughly 180 commits, and 52 revisions of the patch itself before it merged in August. It’s the part of this whole project I’m proudest of.

The old patch page made reviewing feel like doing your taxes. Revisions lived in a sidebar. Comments lived in one place and everything else (new revisions, reviews, merges) lived somewhere else, so you had to piece together what happened yourself. Leaving a review meant leaving a pile of separate comments and hoping they added up to an opinion.

The redesigned patch page.
The redesigned patch page.

One timeline

The core idea is simple: a patch is a story, so tell it in order. The new Activity tab is a single timeline where everything that happened to a patch shows up when it happened. Opening it, every new revision and its commits, comments, reviews, label and state changes, and finally the merge.

The Activity timeline, top to bottom.

Everything hangs off one continuous rail, and every row puts its icon on that rail, so your eye can run straight down the history. The typography does a lot of quiet work: the verbs (“pushed”, “commented”, “accepted”) are set in a lighter tertiary color and the things that matter, the people and the content, carry the weight. It sounds small, but a timeline where every word is the same weight is a wall.

Opened, revisions, commits, comments, reviews and label changes on one rail.
Opened, revisions, commits, comments, reviews and label changes on one rail.

Anything that can expand tells you so. Hover a revision and its icon turns into an expand arrow and the row gets a background. Only rows that actually toggle get that hover state, so you never wonder if something is clickable. Click it and it opens in place with its description, the commits that are new in that revision, and a preview of the diff, animated open with a CSS grid transition.

Expanding a revision in place.
Unfolding collapsed revisions.

Reviews became first-class citizens of the timeline. Each one shows as a pill colored by its verdict (green for accepted, red for rejected, grey when someone only left comments) with the summary right next to it. Expand it and the reviewer’s code comments sit nested underneath, each trimmed to just the lines it’s about, with a toggle for more context.

Review verdicts, and a review expanded with its code comments.
Review verdicts, and a review expanded with its code comments.
Review verdicts, and a review expanded with its code comments.
A review opening up in the timeline.

Changes and reviews

The Changes tab is the diff, built on the Pierre diff engine, with a revision picker, diff settings, and a way to mark files as reviewed as you work through them, so you always know how far along you are.

The Changes tab.
The Changes tab.
Comment on a line to start a review.
Comment on a line to start a review.

Reviews work the way you’d expect from GitHub, which honestly is the point. Click a line of code and leave a comment, and you’ve started a draft review. A bar floats in at the bottom of the window with your pending comments and your file progress. When you’re done you publish everything at once with a verdict. And if you just want to leave a quick note on a line without the whole ceremony, you can do that too.

Starting a review from a line of code.
The draft review bar, and publishing a verdict.
The draft review bar, and publishing a verdict.
The draft review bar, and publishing a verdict.

The header

The top of the page answers “what is this and where is it at” before you scroll. The state (open, draft, archived) is a dropdown right next to the title, and authors can change the state of their own patches. Underneath, one row of metadata leads with the diff stats, which link straight to the Changes tab, then the author, reviewers and labels.

Title, state and metadata.
Title, state and metadata.
Changing a patch’s state.

The conversation

And then the human part. Replies are indented in their own boxes so threads read as threads. Reactions show who reacted, highlight your own, and work on code comments and the patch description too.

A busy thread with replies and reactions.
A busy thread with replies and reactions.

Action buttons only show up on hover, so a long thread isn’t a wall of icons. Hover anyone’s name for a card with what they’ve been working on. And the comment box stays open at the bottom of the page instead of hiding behind a button.

Hover anyone for who they are and what they've been working on.
Hover anyone for who they are and what they’ve been working on.

Issues that match patches

Once patches looked like that, the issue page looked like it came from a different app. I rebuilt it to match: the sidebar is gone, the state control sits next to the title, and the author, labels and assignees form a row of chips underneath. The description gets its own block and the timeline leads with who opened the issue. Authors who aren’t maintainers can now close their own issues, which seems obvious but wasn’t possible before.

The redesigned issue page.
The redesigned issue page.
State next to the title, with author, labels and assignees underneath.
State next to the title, with author, labels and assignees underneath.

The sidebar

The sidebar is where you live in Radicle Desktop: your inbox, your repos, and the issues and patches inside them. It was a fixed column that only got longer as you seeded more repos. I rebuilt it across a handful of patches so it gets out of your way when you want it to and stays organized when you don’t.

The sidebar in action.

Collapse it

The sidebar collapses to a narrow rail of icons. There are two ways to do it, so whatever your hand is already doing works:

The rail keeps only what you reach for all day: Back, Forward and Reload, the inbox, your pinned repos, and the Issues and Patches of the repo you’re in. Everything else (your identity, the full repo list, the guide, settings, node status, counters) waits in the expanded view. Every row is the same square, so an icon never moves depending on the label next to it, and the column of icons lines up with the macOS window buttons. Hover anything on the rail for a tooltip with its name, whether it’s private, and how many open issues or patches it has. Private repos get a small lock badge on their avatar.

Collapsing and expanding with Cmd+B.

Resize it

Drag the edge to make the sidebar anywhere from narrow to wide. The edge tracks your pointer exactly, with no animation lagging behind. Keep dragging past the minimum and it snaps closed into the rail. Drag back out and it opens again, all in a single gesture, so you can close and reopen it without letting go. And if you pull on the collapsed rail but not far enough, it stretches a little and eases back, so it feels like it’s resisting you rather than ignoring you. Both the width and whether it’s collapsed are remembered next time you open the app.

Resize, collapse past the edge, and reopen in one gesture.
The rail resists a pull that doesn’t go far enough.

Organize your repos

The repo context menu.
Confirming before you stop seeding a repo.
Hiding the Guide button.
The repo menu, stopping seeding, and hiding the Guide.

Motion and rhythm

When you open a repo, its Issues and Patches unfurl underneath it. The repo list rolls open and closed the same way. Collapsing the sidebar crossfades between the two views instead of trying to morph every row, label and button into its counterpart, which looked busy and felt slow.

Opening repos, and the repo list rolling open.

Inbox

The inbox was one very long list. If you’d been away for a week it was hundreds of rows of “someone left a comment.” The redesign adds a Latest view sorted by time, collapses long runs of notifications so the overview is actually an overview, and adds an Involves me filter for things you opened, were assigned to, or took part in.

Before: The inbox before, and after, grouped by repository.
After: The inbox before, and after, grouped by repository.
The inbox before, and after, grouped by repository.
Sort by repo or by time, and filter to what involves you.
Sort by repo or by time, and filter to what involves you.
Long runs of updates collapse so the overview stays an overview.
Long runs of updates collapse so the overview stays an overview.

Writing

A lot of Radicle is writing: issues, patch descriptions, comments, reviews. So the editor should feel like every other good Markdown editor. Cmd+B, Cmd+I, Cmd+E and Cmd+K format your selection. Pasting a URL over selected text makes a link. Task list checkboxes are clickable now instead of making you edit - [ ] by hand. Mermaid diagrams, GitHub style alerts and images in READMEs all render.

A Mermaid diagram, an alert, and a clickable task list.
A Mermaid diagram, an alert, and a clickable task list.
A Mermaid diagram, an alert, and a clickable task list.
A Mermaid diagram, an alert, and a clickable task list.

And keyboard shortcuts everywhere, with a ? modal that lists them all.

The keyboard shortcuts modal.
The keyboard shortcuts modal.

Smart links

On GitHub, a link to an issue is a URL on github.com. Radicle has no github.com. The same repo might be on a dozen seeds and viewed through any number of explorers, so a link to one website is a link that can rot. I wanted references that point at the thing, not at a website that happens to show it.

So references are stored as rad: URIs (and did:key: for people), following Radicle’s own spec for them, and the app works out where to show them.

Writing with smart links.

Writing them

Nobody should have to type a URI. In any comment box, @ suggests people, issues and patches, and # narrows that down to just issues and patches. Paste a Radicle ID, a commit hash, a person’s ID or an explorer URL, and the app offers to turn it into a link labelled with the real title or name. Short commit hashes get expanded so they stay unique.

Type @ for anyone or anything, or # for issues and patches.
Type @ for anyone or anything, or # for issues and patches.
Type @ for anyone or anything, or # for issues and patches.
Paste an explorer link and get the real title.
Paste an explorer link and get the real title.

Reading them

Repos, issues, patches, commits and people render as chips, so a reference reads as what it is. Files (down to line ranges), branches, tags and lists render as labelled links. Click one and it opens right in the app if the repo is on your machine, and in your explorer of choice if it isn’t. Right click for the explorer link or the raw rad: URI.

Every kind of reference, rendered.
Every kind of reference, rendered.
Files and branches as labelled links, and the right click menu.
Files and branches as labelled links, and the right click menu.

Opening them from anywhere

The app also registers itself for rad: links, so one clicked in a terminal, a chat app or a browser opens straight in Radicle Desktop. That’s the payoff for storing links this way: they work everywhere, and they never point at a server that might not be there tomorrow.


Making the network visible

The hardest UX problem in a peer to peer app is that nobody knows where their stuff actually is. Did my patch make it to anyone? Is this repo up to date? Who’s allowed to change it? A bunch of my work was about turning invisible network state into something you can see.

The tiny Online/Offline popover became a node window that answers the real question, which is whether your work has reached the network: “Latest patch reached 11 nodes.” You can see and change a repository’s seeding policy, described by whose work you’ll get rather than by refs. And you can set your preferred seeds and which explorer links open in.

The node window, and preferred seeds in Settings.
The node window, and preferred seeds in Settings.
The node window, and preferred seeds in Settings.

Every repository now has a readable identity document: who the delegates are, how many have to sign off on a change, and a timeline of every change to those rules, including who signed and who didn’t. And Releases brought Radicle’s new artifact system to the desktop app, so you can publish a release with files attached and download them verified against their content ID.

The identity document and Releases.
The identity document and Releases.
The identity document and Releases.

One identity, many devices

Here’s a problem you don’t have on a normal website: on Radicle, you are a key. There’s no account on a server, no password, no “log in on your new laptop”. Your identity is a cryptographic key that lives on one machine. So your laptop and your desktop are two different people as far as the network is concerned, and if you lose the machine, you lose yourself.

The protocol team proposed a fix: make a person a repository. Your profile and the set of keys allowed to act as you live in a repo of their own, replicated like everything else, and changing it takes signatures from your other devices. It’s an elegant idea and a really hard thing to explain. My job was to figure out what it would feel like to use, before anyone built it.

So I built a click-through prototype inside the real app, running on fake data, that walked through every flow in the spec. My rule was that nothing goes in the prototype unless it’s planned for the first version, so the team was reacting to the real thing and not a wish list.

Your profile, from the sidebar.
Your profile, from the sidebar.

Your profile, as others see it

A profile has a name, a bio, links and an avatar, like anywhere else. The twist is that it’s published to the network and replicated to everyone who follows you, so old values stay in the history forever. I put a live preview right above the form, showing your hover card and your Explorer page, so what you edit and what other people see are the same thing. And the form tells you plainly that what you write is permanent, before you write your home address in there.

The live preview, as a hover card and as an Explorer page.
The live preview, as a hover card and as an Explorer page.
The live preview, as a hover card and as an Explorer page.
Being honest about what "save" means on a peer to peer network.
Being honest about what “save” means on a peer to peer network.

Keys and two kinds of agreement

This is where it gets subtle. There are two separate rules, and mixing them up would be dangerous. Changing who you are (adding or removing a device) needs a majority of your controlling keys. Changing your profile (your bio, your avatar) only needs the signatures your profile requires. The Keys page keeps those two apart visually so nobody thinks updating their bio needs three devices, or that adding a device only needs one.

Two different rules, kept visibly apart.
Two different rules, kept visibly apart.

Keys also come in two kinds. Some can act as you, some can change who you are, and some do both. A key that can change your identity but never sits on a device is a recovery key. The list shows each key’s role, and if you have fewer than three controlling keys the app warns you, because losing one device shouldn’t lock you out.

Each key's role, and a nudge toward a safer setup.
Each key's role, and a nudge toward a safer setup.
Each key’s role, and a nudge toward a safer setup.

Adding a device

Adding a device goes in one direction only. On the new machine you run a command that prints an enrollment code. On a device you already trust, you approve it, either from a notification or by pasting the code. No comparing fingerprints, no two-way handshake.

Adding a new device, end to end.
The enrollment code from the new device.
The enrollment code from the new device.
The request arriving on a trusted device, and approving it.
The request arriving on a trusted device, and approving it.
The request arriving on a trusted device, and approving it.

A recovery key you can actually use

The spec asks apps to make an offline recovery key easy to set up, so it’s a short guided flow. You get a recovery phrase, confirm you’ve stored it somewhere safe, and sign it into your identity. If your identity needs more than one device to agree, the change is proposed and waits for your other devices to approve it.

Setting up an offline recovery key.
Setting up an offline recovery key.
Setting up an offline recovery key.
Setting up an offline recovery key.

Boards

Seeing your work as a board, the way you would in Linear or GitHub Projects, is something people expect from any project tool. On a normal platform that’s just a feature on a server. On Radicle there is no server, so the question becomes: where does a board live, and who’s allowed to move things on it?

The answer is that a board is its own collaborative object, stored in the repository and replicated peer to peer like issues and patches are. It only stores where each card sits. Titles, labels, state and assignees all come from the issues and patches themselves, so a board can never drift out of sync with the work it shows.

Moving work across the board.

Cards that reflect the real work

An issue card with its priority, a label and a comment count.
An open patch.
A draft patch.
An issue card showing the patch that mentions it.
A card shared from another repository.
An issue, an open patch, a draft patch, an issue with its linked patch, and a card from another repo.

Columns that mean something

A column isn’t just a label. Each one either keeps issues open, closes them as solved, or closes them as something else. Dragging an issue into Done actually closes the issue, for everyone, not just on your board. A repo can have several boards, but the app doesn’t mention that until someone makes a second one.

Choosing what a column does to an issue.
Choosing what a column does to an issue.
Switching boards, and editing columns.
The board picker.
The board picker.

Who can move a card

This was the interesting design problem. With no server, there’s no admin panel deciding permissions. Every peer has to reach the same verdict on its own about whether a move was allowed. The rule we landed on: maintainers can move anything, the author of an issue or patch can move their own card, and anyone assigned to an issue can move it too. An assignee’s move even points back to the moment a maintainer assigned them, as proof every peer can check. Moves that aren’t allowed are simply ignored.

In the interface, that means a card you can’t move doesn’t pretend you can. Pick it up and it stretches a little, shows a lock, and settles back where it was. Press Escape mid-drag and the card goes home too.

A card you’re not allowed to move.
Escape puts a card back where it was.
The lock, and a card's menu.
The lock, and a card's menu.
The lock, and a card’s menu.

A universal navigator

People share Radicle links everywhere: chat, email, issues, the terminal. Those links come in a lot of shapes. There are explorer URLs on any number of different seeds, rad:// URIs, and bare IDs. Getting from one of those to the right place in the app meant clicking through by hand. So I built one place to go anywhere: press Cmd+K.

Searching across every repo.

Paste anything

Paste any Radicle link or ID and the navigator takes you straight to the repo, issue, patch or commit it points at. It doesn’t matter which seed or explorer the link came from. If it points to something that isn’t on your machine yet, it tells you instead of leaving you on a blank page.

Paste a link from anywhere and go there.
When the thing you pasted isn't on your machine.
When the thing you pasted isn’t on your machine.

Search everything

Type instead, and it searches the titles and IDs of every issue and patch across every repo on your machine, with open ones first. With nothing typed, it lists your repos in the same order as the sidebar, pinned ones first, so the shortcut is also the fastest way to switch repos.

Your repos at a glance, and search results across repos.
Your repos at a glance, and search results across repos.
Your repos at a glance, and search results across repos.

Previous project

Logos™️

Next project

Drips