0088: Site favorites, order and groups

On this page

Status: accepted

Context

The Sites screen listed every site by name. With a few dozen sites, the ones you work on every day were somewhere in the middle. People wanted to star a few to keep on top, drag the rest into an order of their own, and gather related sites (a client's, side projects) under a name. The site switcher already had pinned sites, kept as pinned_sites in the preferences, which the Sites screen ignored.

Decision

  • Three kinds of section, in order: Favorites, then your groups, then Other sites. Favorites shows only when there are any. Groups show even when empty, so there's somewhere to drag to.
  • Favorites are the switcher's pins, pinned_sites, renamed Favorites in every place it shows, with a star. A favorite keeps its group or its place in site_order; it shows only under Favorites and goes back to that place when unstarred. Moving it into a group or Other sites unstars it.
  • Kept in config.toml's preferences, beside the favorites: site_order (the sites in no group, by name) and site_groups (id, name, sites, collapsed). A site is in at most one group. Sites with no place yet come after site_order, by name. Repeats, which only a hand-edited file can have, are dropped before any change (first wins, as the screen shows it), so every place counts each site once. This is a handful of names, read with the rest of the preferences, not data that grows, so it doesn't belong in Short Leash's database (0029). A group's name is unique ignoring case, up to 60 characters; its id never changes.
  • Branch workspaces are never placed. They ride under their site (0082); one whose site is gone stays at the end of Other sites.
  • One move per call. POST /v1/sites/layout/place takes a site, where it goes (favorites, ungrouped or a group) and its place among the sites that list shows. /v1/site-groups creates, changes (name, folded, place) and removes groups; creating one with unstar takes its favorites out of Favorites in the same change, as the app's New group with these sites does. Sending a whole new layout would let two windows, or the app and an agent, undo each other's moves. POST /v1/preferences no longer writes the favorites, order or groups at all.
  • Names are forgotten only when a site is definitely gone: unlinked, or its parked folder forgotten. A rename follows everywhere. A parked folder that's missing for a while (an unmounted disk) keeps its place.
  • The app shows a change before Short Leash answers, by leashd's own rules (ipc/siteLayoutRules.ts, which the mocked daemon uses too), and Short Leash's answer replaces it. A refused move puts things back, with the refusal's toast.
  • Dragging is the app's own, with pointer events, not HTML5 drag and drop or a library. The list is virtualized, so the dragged row is kept drawn while it's out of range; a library's drag would need every row in the page. HTML5 drag shows the system's ghost image, which can't lift, settle or follow the row's look. Option and an arrow on a row's grip do the same from the keyboard, and a screen reader hears where the site landed.
  • Sort is a view choice: Custom (your order, the only one you can drag in), Name, or Running first, each sorting inside the sections. It's remembered per window in local storage, not in the preferences.

Consequences

  • Favorites, in their order, come first in the site switcher and the menu bar; the menu bar lists the rest of its sites in the Sites screen's order. The switcher's Running and Recent keep their own order, Home's sections keep sorting by what needs attention, and leash sites and the agents' list_sites keep name order.
  • The motion is new for the app (lift, rows making room, settle, a folded group opening under a drag, rows gliding to a new sort) and is on the Sites motion board; CLAUDE.md's motion list names it. With Reduce Motion on, a dragged row still follows the pointer, and the rest is instant.
  • Nothing here leaves the computer.
  • A group can't be nested in another, and a site can't be in two groups. Both would make "where does this land" harder to answer while dragging.