From 45d8552bcab8c000e3688ed02368dca4d971980d Mon Sep 17 00:00:00 2001 From: Daniel Ehrhardt Date: Wed, 28 Oct 2026 21:50:24 +0100 Subject: [PATCH 1/5] Remove the Runners page and its dialogs from the desktop app Runners were superseded by device groups. Drop the /runners route, the sidebar, easy-mode and command palette entries, the add/health/update/screen/rename dialogs, and the api client methods and query keys only they used. --- apps/desktop/src/App.tsx | 2 - .../src/components/layout/app-shell.tsx | 3 - .../src/components/layout/command-palette.tsx | 4 - .../components/runners/add-runner-dialog.tsx | 372 ----------------- .../src/components/runners/network-picker.tsx | 190 --------- .../src/components/runners/runner-card.tsx | 375 ------------------ .../src/components/runners/runner-dialogs.tsx | 217 ---------- .../src/components/runners/runner-health.tsx | 301 -------------- .../src/components/runners/runner-parts.tsx | 190 +-------- .../runners/runner-screen-dialog.tsx | 116 ------ .../src/components/runners/runner-update.tsx | 117 ------ .../components/runners/use-runner-actions.ts | 149 ------- .../components/settings/license-section.tsx | 2 +- .../src/components/settings/logs-section.tsx | 2 +- .../components/tasks/fix-in-code-dialog.tsx | 19 +- apps/desktop/src/lib/api.ts | 27 +- apps/desktop/src/lib/mode.ts | 1 - apps/desktop/src/lib/queryKeys.ts | 3 - apps/desktop/src/lib/realtime.ts | 1 - .../src/pages/runners/runners-page.tsx | 119 ------ 20 files changed, 13 insertions(+), 2197 deletions(-) delete mode 100644 apps/desktop/src/components/runners/add-runner-dialog.tsx delete mode 100644 apps/desktop/src/components/runners/network-picker.tsx delete mode 100644 apps/desktop/src/components/runners/runner-card.tsx delete mode 100644 apps/desktop/src/components/runners/runner-dialogs.tsx delete mode 100644 apps/desktop/src/components/runners/runner-health.tsx delete mode 100644 apps/desktop/src/components/runners/runner-screen-dialog.tsx delete mode 100644 apps/desktop/src/components/runners/runner-update.tsx delete mode 100644 apps/desktop/src/components/runners/use-runner-actions.ts delete mode 100644 apps/desktop/src/pages/runners/runners-page.tsx 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 ( - - - - Add a runner - Another Mac that does the work of a chat — so it goes on while this one sleeps. - - - - {runner ? ( - - onOpenChange(false)} /> - - ) : byCode ? ( - - setByCode(false)} - onPaired={(r) => { - void upsertRunner(qc, r); - setPaired(r); - }} - /> - - ) : ( - -
    - - A Mac mini, or a laptop that stays plugged in. - - - {offer && !create.isError ? ( -
    - setAddress(r.address)} - onRefresh={() => create.mutate()} - refreshing={create.isPending} - /> -
    - ) : create.isError ? ( - "On the same network, or from anywhere through Tailscale." - ) : ( - - )} -
    - - {offer && !create.isError ? ( -
    - {command ? ( - setSource(v as Source)} className="gap-2.5"> - - Install from {THIS_COMPUTER_INLINE} - Install from usegodmode.com - - - -

    - It downloads Godmode from this computer - {route?.network === "tailscale" ? " through Tailscale" : ""}, installs it as a runner and pairs it. The command works once. -

    -
    - - - -
    - ) : ( - - )} -
    - - Works for {Math.floor(remaining / 60)}:{String(remaining % 60).padStart(2, "0")} - -
    -
    - ) : create.isError ? ( -
    -

    Couldn't make the install command

    -

    {errorMessage(create.error)}

    - -
    - ) : ( - - )} -
    - - The runner appears here when the command finishes — usually under a minute. The first time, it also installs Claude Code and a browser. - -
- - {offer && !create.isError && ( -
- - - Waiting for the runner - {waitingOn ? ( - <> - {" "} - on {waitingOn.replace(/^https?:\/\//, "")} - - ) : null} - {route?.network === "tailscale" ? " through Tailscale" : null} - - - {[0, 1, 2].map((i) => ( - - ))} - -
- )} - -

- {create.isError ? "Already have a code from the runner?" : "Did the command print a code instead?"}{" "} - -

-
- )} -
-
-
- ); -} - -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 ( -
    -
    -

    Paste the runner's pairing code

    -

    - -

    -
    -