diff --git a/README.md b/README.md
index c06a3860..194d86e0 100644
--- a/README.md
+++ b/README.md
@@ -138,7 +138,7 @@ API key (Max and Team plans now include monthly API credits) or a Team plan. God
## More
- [docs/FEATURES.md](docs/FEATURES.md): every feature with screenshots, a quick start, and setup guides for the desktop
- app, the headless server and Docker, the phone app, Godmode Cloud, runners, computer use, macOS VMs, SSH servers,
+ app, the headless server and Docker, the phone app, Godmode Cloud, computer use, macOS VMs, SSH servers,
plugins and connecting Claude Code over MCP.
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): how the core, runner, vault, browser and scheduler fit together.
- [SECURITY.md](SECURITY.md): the full security model and its caveats.
diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx
index aac1d7ee..b0159cd5 100644
--- a/apps/desktop/src/App.tsx
+++ b/apps/desktop/src/App.tsx
@@ -41,7 +41,6 @@ const VmsPage = lazy(() => import("@/pages/vms/vms-page"));
const SshPage = lazy(() => import("@/pages/ssh/ssh-page"));
const AndroidDevicePage = lazy(() => import("@/pages/android/android-device-page"));
const CallsPage = lazy(() => import("@/pages/calls/calls-page"));
-const RunnersPage = lazy(() => import("@/pages/runners/runners-page"));
const InboxPage = lazy(() => import("@/pages/inbox/inbox-page"));
const SettingsPage = lazy(() => import("@/pages/settings/settings-page"));
@@ -226,7 +225,6 @@ export function App() {
} />
: } />
} />
- } />
} />
} />
} />
diff --git a/apps/desktop/src/components/layout/app-shell.tsx b/apps/desktop/src/components/layout/app-shell.tsx
index 7537b31f..c48f6662 100644
--- a/apps/desktop/src/components/layout/app-shell.tsx
+++ b/apps/desktop/src/components/layout/app-shell.tsx
@@ -16,7 +16,6 @@ import {
Lock,
MessageSquarePlus,
MessagesSquare,
- MonitorSmartphone,
PanelLeft,
PanelLeftClose,
PanelLeftOpen,
@@ -198,7 +197,6 @@ export function AppShell({ children }: { children: ReactNode }) {
{ to: "/computer", label: "Computer", icon: },
{ to: "/vms", label: "Virtual machines", icon: },
{ to: "/ssh", label: "SSH servers", icon: },
- { to: "/runners", label: "Runners", icon: },
];
// Easy mode: the everyday pages in plain words up front, every other page under More.
const byPath = new Map([...workNav, ...machineNav].map((item) => [item.to, item]));
@@ -219,7 +217,6 @@ export function AppShell({ children }: { children: ReactNode }) {
easyItem("/computer", "Computer control"),
easyItem("/vms", "Virtual machines"),
easyItem("/ssh", "Servers"),
- easyItem("/runners", "Other computers"),
];
return (
diff --git a/apps/desktop/src/components/layout/command-palette.tsx b/apps/desktop/src/components/layout/command-palette.tsx
index 8bc9964a..f7ff92ab 100644
--- a/apps/desktop/src/components/layout/command-palette.tsx
+++ b/apps/desktop/src/components/layout/command-palette.tsx
@@ -29,7 +29,6 @@ import {
Lock,
MessageCircle,
MessageSquarePlus,
- MonitorSmartphone,
MonitorUp,
Moon,
Phone,
@@ -418,9 +417,6 @@ export function CommandPalette() {
Android phones
)}
- go("/runners")}>
- {named("/runners", "Runners")}
- go("/workspaces")}>
Workspaces
diff --git a/apps/desktop/src/components/runners/add-runner-dialog.tsx b/apps/desktop/src/components/runners/add-runner-dialog.tsx
deleted file mode 100644
index 07e02537..00000000
--- a/apps/desktop/src/components/runners/add-runner-dialog.tsx
+++ /dev/null
@@ -1,372 +0,0 @@
-import { useEffect, useState, type FormEvent, type ReactNode } from "react";
-import { useMutation, useQueryClient } from "@tanstack/react-query";
-import { AnimatePresence, motion } from "motion/react";
-import { ArrowLeft, ClipboardPaste, MonitorSmartphone, RefreshCw, TriangleAlert } from "lucide-react";
-import { parseRunnerCode, RUNNER_CODE_PREFIX, type RemoteRunner, type RunnerPairingOffer } from "@godmode/shared";
-import { Button } from "@/components/ui/button";
-import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
-import { Skeleton } from "@/components/ui/skeleton";
-import { Spinner } from "@/components/ui/spinner";
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
-import { Textarea } from "@/components/ui/textarea";
-import { DrawCheck } from "@/components/aicss/Motion";
-import { InlineCode } from "@/components/onboarding/doctor-checklist";
-import { ExpiryRing } from "@/components/settings/pair-phone-dialog";
-import { useNow } from "@/components/vault/use-now";
-import { api, errorMessage } from "@/lib/api";
-import { useRunners } from "@/lib/hooks";
-import { onServerEvent, upsertRunner } from "@/lib/realtime";
-import { cn } from "@/lib/utils";
-import { NetworkPicker, pickRoute } from "./network-picker";
-import { RunnerHealthPanel, useRunnerHealth } from "./runner-health";
-import { CommandBlock, THIS_COMPUTER, THIS_COMPUTER_INLINE, blockingChecks } from "./runner-parts";
-import { useRunnerActions } from "./use-runner-actions";
-
-/** An offer is good for ten minutes; the ring empties over that time. */
-const OFFER_SECONDS = 600;
-const LICENSE_PLACEHOLDER = /GM-X{5}(?:-X{5}){3}/;
-
-type Source = "local" | "website";
-
-/**
- * Add a runner: one command, run on the other Mac, installs Godmode there and pairs it with this computer. The dialog
- * waits for it, then shows the runner getting ready. A pairing code pasted by hand works too.
- */
-export function AddRunnerDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) {
- const qc = useQueryClient();
- const { data: runners } = useRunners();
- const [offer, setOffer] = useState(null);
- const [paired, setPaired] = useState(null);
- const [byCode, setByCode] = useState(false);
- const [source, setSource] = useState("local");
- const [address, setAddress] = useState(null);
- const now = useNow(1000);
-
- const create = useMutation({ mutationFn: api.runners.pairing, onSuccess: setOffer });
-
- useEffect(() => {
- if (!open) return;
- setPaired(null);
- setOffer(null);
- setByCode(false);
- setSource("local");
- setAddress(null);
- create.mutate();
- const off = onServerEvent((e) => {
- if (e.type === "runner.paired") setPaired(e.runner);
- });
- return () => {
- off();
- void api.runners.cancelPairing().catch(() => undefined);
- };
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [open]);
-
- const remaining = offer ? Math.max(0, Math.round((new Date(offer.expiresAt).getTime() - now) / 1000)) : 0;
- const expired = !!offer && remaining === 0;
-
- // A command that ran out is replaced by a fresh one while the dialog waits.
- useEffect(() => {
- if (open && expired && !paired && !create.isPending) create.mutate();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [open, expired, paired]);
-
- const route = offer ? pickRoute(offer.routes, address) : null;
- const command = route?.command ?? null;
- const waitingOn = route?.url ?? offer?.urls[0] ?? null;
-
- // The list is kept live: the paired runner's connection and health follow it.
- const runner = paired ? (runners?.find((r) => r.id === paired.id) ?? paired) : null;
-
- return (
-
- );
-}
-
-function Step({ n, title, children }: { n: number; title: string; children: ReactNode }) {
- return (
-
- {n}
-
-
{title}
-
{children}
-
-
- );
-}
-
-/** The install command through usegodmode.com, with the license placeholder marked: that part is the human's to fill in. */
-function WebsiteCommand({ command }: { command: string }) {
- const match = LICENSE_PLACEHOLDER.exec(command);
- return (
- <>
-
- {match ? (
- <>
- {command.slice(0, match.index)}
- {match[0]}
- {command.slice(match.index + match[0].length)}
- >
- ) : undefined}
-
-
- {match ? (
- <>
- Replace the highlighted part with your license key.{" "}
- >
- ) : null}
- The other Mac downloads Godmode from usegodmode.com, installs it as a runner and pairs it. The command works once.
-
- >
- );
-}
-
-/** The other way to pair: the runner printed a `gmr1.` code (the command couldn't reach this computer, or `godmode runner pair`). */
-function CodeForm({ now, onBack, onPaired }: { now: number; onBack: () => void; onPaired: (runner: RemoteRunner) => void }) {
- const [code, setCode] = useState("");
- const pair = useMutation({ mutationFn: (text: string) => api.runners.pair(text), onSuccess: onPaired });
-
- const text = code.trim();
- const parsed = text ? parseRunnerCode(text) : null;
- const expired = !!parsed && parsed.exp * 1000 < now;
- const problem = !text
- ? null
- : !parsed
- ? `That isn't a pairing code. It starts with \`${RUNNER_CODE_PREFIX}\` — copy all of it, line breaks don't matter.`
- : expired
- ? "This code has expired. Run `godmode runner pair` on the runner for a new one."
- : null;
- const ready = !!parsed && !expired;
-
- const submit = (e: FormEvent) => {
- e.preventDefault();
- if (ready && !pair.isPending) pair.mutate(text);
- };
-
- return (
-
- );
-}
-
-/** After pairing: the runner sets itself up, and the checks turn green one by one while the human watches. */
-function PairedView({ runner, onDone }: { runner: RemoteRunner; onDone: () => void }) {
- const actions = useRunnerActions();
- const health = useRunnerHealth(runner);
- const online = runner.state === "online";
- const ready = online && !!health.data && blockingChecks(health.data).length === 0;
-
- return (
- <>
-
-
-
-
-
-
{runner.name} is paired
-
- {ready
- ? `It's ready. Start a chat and pick ${runner.name} where it says “${THIS_COMPUTER}”.`
- : online
- ? "It's getting ready: your agents and logins are copied over, and it installs Claude Code and a browser. That takes a few minutes the first time."
- : `Godmode is connecting to it. If that takes long, check that ${runner.name} is awake and on the same network.`}
-
-
-
-
-
-
-
-
- {ready ? "You find it under Runners from now on." : "You can close this — it carries on by itself, and Runners shows how far it is."}
-
-
-
- >
- );
-}
diff --git a/apps/desktop/src/components/runners/network-picker.tsx b/apps/desktop/src/components/runners/network-picker.tsx
deleted file mode 100644
index ad28bf33..00000000
--- a/apps/desktop/src/components/runners/network-picker.tsx
+++ /dev/null
@@ -1,190 +0,0 @@
-import { useId } from "react";
-import { Box, ExternalLink, RefreshCw, Wifi } from "lucide-react";
-import type { RunnerNetwork, RunnerOfferRoute, TailscaleStatus } from "@godmode/shared";
-import { Button } from "@/components/ui/button";
-import { Label } from "@/components/ui/label";
-import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
-import { TAILSCALE_DOWNLOAD } from "@/components/settings/pair-phone-dialog";
-import { openExternal } from "@/lib/desktop";
-import { cn } from "@/lib/utils";
-
-const STORAGE_KEY = "godmode-runner-network";
-
-const TITLE: Record = { lan: "Same network", vm: "Virtual machine", tailscale: "Tailscale" };
-
-/** The network the human picked last time; the next offer starts there. */
-export function preferredNetwork(): RunnerNetwork | null {
- try {
- const v = localStorage.getItem(STORAGE_KEY);
- return v === "lan" || v === "vm" || v === "tailscale" ? v : null;
- } catch {
- return null;
- }
-}
-
-function rememberNetwork(network: RunnerNetwork) {
- try {
- localStorage.setItem(STORAGE_KEY, network);
- } catch {
- /* private mode */
- }
-}
-
-/** The route to show: the one picked, else one on the network picked last time, else the best. */
-export function pickRoute(routes: RunnerOfferRoute[], address: string | null): RunnerOfferRoute | null {
- const network = preferredNetwork();
- return routes.find((r) => r.address === address) ?? routes.find((r) => r.network === network) ?? routes[0] ?? null;
-}
-
-/** A Tailscale address (100.64.0.0/10) or MagicDNS name. */
-export function isTailscaleHost(host: string | null | undefined): boolean {
- if (!host) return false;
- if (/\.ts\.net\.?$/i.test(host)) return true;
- const [a, b] = host.split(".").map(Number);
- return a === 100 && b! >= 64 && b! <= 127;
-}
-
-/** Tailscale's mark: nine dots, the bottom-right ones faded. */
-export function TailscaleMark({ className }: { className?: string }) {
- const dots = [
- [0, 0, 0.35],
- [1, 0, 0.35],
- [2, 0, 0.35],
- [0, 1, 1],
- [1, 1, 1],
- [2, 1, 1],
- [0, 2, 0.35],
- [1, 2, 1],
- [2, 2, 0.35],
- ] as const;
- return (
-
- );
-}
-
-function NetworkIcon({ network, className }: { network: RunnerNetwork; className?: string }) {
- if (network === "tailscale") return ;
- const Icon = network === "vm" ? Box : Wifi;
- return ;
-}
-
-function hint(route: RunnerOfferRoute): string {
- if (route.network === "tailscale") return route.detail ?? "From anywhere, through your tailnet";
- if (route.network === "vm") return `A Mac running in a VM on this one${route.detail ? ` · ${route.detail}` : ""}`;
- return `Wi-Fi or Ethernet${route.detail ? ` · ${route.detail}` : ""}`;
-}
-
-/**
- * How the other Mac reaches this one: each of this computer's addresses, Tailscale with its MagicDNS name. When
- * Tailscale isn't connected here, it says why and how to get it.
- */
-export function NetworkPicker({
- routes,
- tailscale,
- value,
- onChange,
- onRefresh,
- refreshing,
-}: {
- routes: RunnerOfferRoute[];
- tailscale: TailscaleStatus;
- value: RunnerOfferRoute | null;
- onChange: (route: RunnerOfferRoute) => void;
- onRefresh: () => void;
- refreshing: boolean;
-}) {
- const hasTailscale = routes.some((r) => r.network === "tailscale");
- return (
-
{tailscale.running ? "Tailscale has no address on this Mac yet." : tailscale.detail}
-
-
- {!tailscale.installed && (
-
- )}
-
-
-
- )}
-
- {value?.network === "tailscale" && (
-
- The other Mac needs Tailscale too, signed in to the same account
- {tailscale.tailnet ? (
- <>
- {" "}
- ({tailscale.tailnet})
- >
- ) : null}
- . Then it reaches this Mac from anywhere, not only at home.
-
- )}
-
- );
-}
-
-function OptionTile({ network, muted, checked }: { network: RunnerNetwork; muted?: boolean; checked?: boolean }) {
- return (
-
-
-
- );
-}
-
-function RouteOption({ route, checked }: { route: RunnerOfferRoute; checked: boolean }) {
- const id = useId();
- return (
-
- );
-}
diff --git a/apps/desktop/src/components/runners/runner-card.tsx b/apps/desktop/src/components/runners/runner-card.tsx
deleted file mode 100644
index 20849735..00000000
--- a/apps/desktop/src/components/runners/runner-card.tsx
+++ /dev/null
@@ -1,375 +0,0 @@
-import { useEffect, useRef, useState } from "react";
-import { AnimatePresence, motion } from "motion/react";
-import { CircleArrowUp, CircleCheck, Cookie, Ellipsis, HeartPulse, KeyRound, Monitor, MonitorSmartphone, Network, Pencil, RefreshCw, Sparkles, Trash2, TriangleAlert, Wrench } from "lucide-react";
-import type { RemoteRunner } from "@godmode/shared";
-import { Button } from "@/components/ui/button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { Spinner } from "@/components/ui/spinner";
-import { Switch } from "@/components/ui/switch";
-import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
-import { LiveDot } from "@/components/aicss/Motion";
-import { CopyButton } from "@/components/chat/copy-button";
-import { cn } from "@/lib/utils";
-import { TailscaleMark, isTailscaleHost } from "./network-picker";
-import { RunnerHealthPanel, useRunnerHealth } from "./runner-health";
-import {
- CHECK_COLOR,
- CHECK_ICON,
- HealthPill,
- RunnerStateBadge,
- latestSummary,
- runnerEndpoint,
- runnerPlatform,
- runnerProblem,
- runnerSyncLine,
- runnerVerdict,
- runnerWorkLine,
-} from "./runner-parts";
-import { RunnerUpdateStrip, hasUpdateNews, shortBuild } from "./runner-update";
-import type { RunnerActions } from "./use-runner-actions";
-
-const FOOTER_BUTTON = "h-7 gap-1.5 px-2 text-[12.5px] font-normal text-muted-foreground hover:text-foreground [&_svg]:size-3.5";
-
-export function RunnerCard({
- runner,
- actions,
- onRename,
- onAddresses,
- onRemove,
- onScreen,
- onFixInCode,
-}: {
- runner: RemoteRunner;
- actions: RunnerActions;
- onRename: () => void;
- onAddresses: () => void;
- onRemove: () => void;
- onScreen: () => void;
- onFixInCode: () => void;
-}) {
- const online = runner.state === "online";
- const [healthOpen, setHealthOpen] = useState((runner.health?.failing ?? 0) > 0);
- // The report is loaded while the checks are open — and once by itself when nobody has looked at this runner yet, so
- // the card never just says "not checked". The pill shows the fresher of this report and the list's summary.
- const health = useRunnerHealth(runner, { enabled: healthOpen || !runner.health });
- const summary = latestSummary(runner.health, health.data);
- const failing = summary?.failing ?? 0;
- // Something keeps it from working: the checks open by themselves, once — that is where the fixes are.
- const opened = useRef(healthOpen);
- useEffect(() => {
- if (failing === 0 || opened.current) return;
- opened.current = true;
- setHealthOpen(true);
- }, [failing]);
- const working = runner.activeRuns > 0;
- const verdict = runnerVerdict(runner, summary);
- const problem = runnerProblem(runner);
- const sync = runnerSyncLine(runner);
- const platform = runnerPlatform(runner);
- const endpoint = runnerEndpoint(runner);
- const connecting = actions.isBusy("connect", runner.id) || runner.state === "connecting";
- const syncing = actions.isBusy("sync", runner.id) || runner.sync.state === "syncing";
- const rechecking = actions.isBusy("health", runner.id);
- const checking = rechecking || (online && !summary && health.isFetching);
- const repairing = actions.isBusy("autofix", runner.id);
- const panelId = `runner-health-${runner.id}`;
- const VerdictIcon = verdict.tone === "busy" ? null : CHECK_ICON[verdict.tone];
-
- return (
-
-
-
-
-
-
-
- {online ? (
-
- ) : (
-
- )}
-
-
-
-
-
-
{runner.name}
-
-
-
- {platform ? {platform} : Hasn't connected yet}
- {runner.version && (
- <>
-
- ·
-
-
- Godmode {runner.version}
- {shortBuild(runner.build) && {shortBuild(runner.build)}}
-
- {online && runner.update.state === "current" && !runner.update.tools.length && (
-
- Up to date
-
-
-
-
- Check for updates of Godmode and its tools, and install them
-
-
- )}
- >
- )}
-
-
-
- {runner.fingerprint}
-
- The key of {runner.hostname || runner.name}, pinned when you paired it. Godmode only talks to the computer that holds it, and everything between the two is
- end-to-end encrypted.
-
-
-
-
-
-
-
-
- Look at its software, permissions and access again
-
-
-
-
-
- Send your agents, logins, integrations and settings again — Godmode also does this by itself
-
-
-
-
-
- See its screen and use its mouse and keyboard
-
-
-
-
-
- Start a chat that looks at this runner and repairs it
-
-
-
-
-
- Send its log to a ticket: an agent fixes the causes in Godmode's code and merges the fix
-
-
-
-
- {healthOpen && (
-
-
-
- )}
-
-
- );
-}
-
-function RunnerMenu({ runner, actions, onRename, onAddresses, onRemove }: { runner: RemoteRunner; actions: RunnerActions; onRename: () => void; onAddresses: () => void; onRemove: () => void }) {
- const pending = actions.isBusy("update", runner.id) || actions.isBusy("remove", runner.id);
- return (
-
-
-
-
-
-
- Rename…
-
-
- Addresses…
-
-
- {/* Stays open on a click: the switch is the answer. */}
- {
- e.preventDefault();
- actions.update.mutate({ runner, patch: { syncBrowser: !runner.syncBrowser } });
- }}
- >
-
-
- Copy browser sessions
- Chats start there signed in to the same sites as here.
-
-
-
- {
- e.preventDefault();
- actions.update.mutate({ runner, patch: { autoUpdate: !runner.update.autoUpdate } });
- }}
- >
-
-
- Update automatically
- Gets this computer's Godmode once its chats are done.
-
-
-
- actions.upgrade.mutate(runner)}>
- {runner.update.state === "available" || runner.update.tools.length ? "Update now" : "Check for updates"}
-
-
-
- Remove…
-
-
-
- );
-}
diff --git a/apps/desktop/src/components/runners/runner-chip.tsx b/apps/desktop/src/components/runners/runner-chip.tsx
index 4d0b3bbc..cc1f6658 100644
--- a/apps/desktop/src/components/runners/runner-chip.tsx
+++ b/apps/desktop/src/components/runners/runner-chip.tsx
@@ -1,7 +1,7 @@
import { useState } from "react";
import { Link, useNavigate } from "react-router";
import { useMutation, useQueryClient } from "@tanstack/react-query";
-import { Check, ChevronDown, Laptop, MonitorSmartphone, Plus, RefreshCw, Settings2, TriangleAlert, WifiOff } from "lucide-react";
+import { Check, ChevronDown, Laptop, MonitorSmartphone, RefreshCw, Settings2, TriangleAlert, WifiOff } from "lucide-react";
import type { RemoteRunner } from "@godmode/shared";
import { Button } from "@/components/ui/button";
import { Command, CommandGroup, CommandItem, CommandList, CommandSeparator } from "@/components/ui/command";
@@ -11,7 +11,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
import { timeAgo } from "@/components/ssh/ssh-parts";
import { toastApiError } from "@/components/vault/vault-utils";
import { api } from "@/lib/api";
-import { isRunnerPlace, placeName, usePlaces, type Place } from "@/lib/places";
+import { isOlderRunner, isRunnerPlace, placeName, usePlaces, type Place } from "@/lib/places";
import { upsertRunner } from "@/lib/realtime";
import { cn } from "@/lib/utils";
import { RUNNER_STATE_LABEL, RunnerStateDot, RunnerTag, THIS_COMPUTER, THIS_COMPUTER_INLINE } from "./runner-parts";
@@ -44,19 +44,17 @@ function PlaceIcon({ place }: { place: Place }) {
}
/**
- * Composer control of a new chat for where it works: this computer, another computer of the device group, or a runner.
- * Hidden while there is nowhere else. A place that isn't online is listed but can't be picked.
+ * Composer control of a new chat for where it works: this computer or another computer of the device group. Hidden
+ * while there is nowhere else. A place that isn't online is listed but can't be picked.
*/
export function RunnerChip({ value, onChange }: { value: string | null; onChange: (runnerId: string | null) => void }) {
- const { places, hereId, inGroup } = usePlaces();
+ const { places, hereId } = usePlaces();
const navigate = useNavigate();
const [open, setOpen] = useState(false);
- if (places.length <= 1) return null;
+ const others = places.filter((p) => !p.here && !isOlderRunner(p));
+ if (!others.length) return null;
- const selected = places.find((p) => p.runnerId === value) ?? places.find((p) => p.here)!;
- const current = selected.here ? null : selected;
- const others = places.filter((p) => !p.here && p.kind !== "runner");
- const runners = places.filter((p) => !p.here && p.kind === "runner");
+ const current = others.find((p) => p.runnerId === value) ?? null;
const pick = (runnerId: string | null) => {
setOpen(false);
if (runnerId !== value) onChange(runnerId);
@@ -75,14 +73,10 @@ export function RunnerChip({ value, onChange }: { value: string | null; onChange
@@ -90,7 +84,7 @@ export function RunnerChip({ value, onChange }: { value: string | null; onChange