Skip to content

Menu Bar

tavlean edited this page Jul 26, 2026 · 6 revisions

Menu Bar

The menubar command (src/menubar.tsx). Non-obvious decisions only; the feature list lives in the README.

Not a long-lived process

Menu bar commands are loaded on demand: Raycast runs the command when the user opens the menu or when the 1m background interval fires, waits for isLoading to go false, then unloads it. There is no resident process and no shared React state with the dashboard. Every piece of "live" data is re-derived per run.

Consequence: the menu paints from a Cache snapshot first (instant, possibly stale), then fetchServers() replaces it in the same open. The snapshot lives in src/snapshot.ts and is also written by the dashboard's poll loop, so an open dashboard keeps the menu's first paint fresh.

The refresh reads the persisted pending store (src/pendingStore.ts) first and passes the mid-start cwds to fetchServers as its settlingCwds, ignoring entries past their deadline. That is how the count avoids including a starting project's ephemeral-port helpers, the same call the dashboard makes; see Process Detection, rule 4. The dashboard's snapshot is filtered at the source too, so the first paint agrees.

Why the title count needs pokes

The menu bar title (the running count) only changes when the command re-renders. Waiting for the 1m interval would leave a killed server in the count for up to a minute, so the dashboard pokes the command awake after kill / restart / spawn-detected with launchCommand({ name: "menubar", type: LaunchType.Background }).

The menu bar does NOT poke itself after its own kill/restart actions (PR review finding, 2026-07-02): the handler's refresh() already re-renders the still-loaded command with the fresh count, and Raycast drops overlapping background launches of the same command, so a self-poke is a likely no-op that can only add a redundant process scan.

pokeMenuBar() wraps the call in try/catch because launchCommand throws when the user has disabled the menu bar command, and a disabled menu bar must never break a dashboard kill.

Starts go through the dashboard

Menu items run arbitrary onAction code, so kills and restarts call killServer / restartServer directly: instant, no window. Kills also call reapProjectHelpersWhenDown afterwards, the same helper cleanup the dashboard's kills run (see Process Detection). Starts do not spawn inline. A start's outcome takes seconds and needs bind-watching, a pending row, and log capture, all of which the dashboard's spawn state machine already owns (see Spawn Flow). So a start item launches the dashboard with the same single-target launchContext.spawn payload the picker uses, and the user watches the server come up on its row. Re-implementing a silent spawn path in the menu bar would fork that machinery for a worse experience.

Kill All without a confirm

Per-project "Kill All N Servers" diverges from the dashboard rule that bulk kills confirm first. That is deliberate, decided with the user (2026-07-02): confirmAlert is built for commands running in the Raycast window and is not part of the menu bar surface, so the choice was between no bulk kill, a hidden Option-key variant, and a visible unconfirmed item. The dashboard confirms because ⌃⇧X is one fat-fingerable keystroke from anywhere; a menu click on an item that names its own blast radius is already deliberate. Guardrails instead of a dialog:

  • shown only when the project runs 2+ servers (with one server, the per-server Kill is the same action),
  • the count lives in the label ("Kill Both Servers" for a pair, "Kill All 3 Servers" beyond), bottom placement per the dashboard's high-blast-radius convention (UI Conventions),
  • recovery is cheap by design: the killed project immediately surfaces in the frecency-ranked Start section.

The kill loop uses Promise.allSettled: menu data is a snapshot, so a server can die between menu open and click, and one stale pid must not stop the rest.

Frecency namespace

The Start section sorts recents with useFrecencySorting (namespace project-starts, key = canonical cwd) and calls visitItem on every start. The Start picker is expected to adopt the same namespace (Roadmap Phase 5), so ranking accrues across surfaces from day one.

Assorted constraints

  • Item titles at the same menu level must be unique or onAction can misfire (documented Raycast behavior). Server submenu titles are host plus port, and ports are unique, so this holds without extra work.
  • interval: "1m": the manifest docs state a 1m minimum (the background-refresh page says 10s; the manifest wins for store safety).
  • updateCommandMetadata({ subtitle: "N running" }) keeps the root-search row informative for free on every run.
  • Menu bar commands are macOS-only, which is fine today and noted against the Windows port in the Roadmap.

Auto-open works the same from every entry point

Menu-bar starts once never opened the browser while identical dashboard starts did. Root cause and fix (2026-07-26): the dashboard reads the extension-level autoOpenInBrowser preference itself, and no caller passes a copy through the launch context anymore. History in Auto-Open Investigation, mechanism in Spawn Flow.

Clone this wiki locally