Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Merge branch 'main' into rm-agents | 1 | /** |
| 2 | * A button at work, for a button that asks the desktop app to do | |
| 3 | * something whose effect shows later, in a status the app sends: Retry, | |
| 4 | * Start, Stop, Check for runner updates, Restart now. From the click it is | |
| 5 | * busy (a spinner, pressed no more, `aria-busy`) until the status it waits | |
| 6 | * on changes, the app says it couldn't, or `BUSY_LIMIT` passes with no | |
| 7 | * change, so a press is never seen to do nothing and never leaves a | |
| 8 | * button dead. The app's own pages do the same (apps/desktop launcher/busy.js). | |
| 9 | */ | |
| 10 | import { useCallback, useEffect, useState } from "react"; | |
| 11 | ||
| 12 | /** How long a button waits for the status that follows before it is given back. */ | |
| 13 | export const BUSY_LIMIT = 20_000; | |
| 14 | ||
| 15 | /** A press: the status as it was when the button was pressed, and when. */ | |
| 16 | export type Press = { status: string; at: number }; | |
| 17 | ||
| 18 | /** Whether a press still waits: the status is as it was, and not for too long. */ | |
| 19 | export function stillBusy(press: Press | null, status: string, now: number): boolean { | |
| 20 | return press !== null && press.status === status && now - press.at < BUSY_LIMIT; | |
| 21 | } | |
| 22 | ||
| 23 | /** | |
| 24 | * A button's busy state, waiting on `status` (any string that changes when | |
| 25 | * what the button asked for shows). `run` does the work: with `waits`, the | |
| 26 | * button stays busy until the status changes; without, until the work is | |
| 27 | * done. A failure gives the button back and goes to `onError`. | |
| 28 | */ | |
| 29 | export function useBusy(status: string, onError?: (error: unknown) => void) { | |
| 30 | const [press, setPress] = useState<Press | null>(null); | |
| 31 | const [now, setNow] = useState(0); | |
| 32 | // Given back after the limit, with nothing heard. | |
| 33 | useEffect(() => { | |
| 34 | if (!press) return; | |
| 35 | const timer = window.setTimeout(() => setNow(Date.now()), Math.max(0, press.at + BUSY_LIMIT - Date.now())); | |
| 36 | return () => window.clearTimeout(timer); | |
| 37 | }, [press]); | |
| 38 | const busy = stillBusy(press, status, Math.max(now, press?.at ?? 0)); | |
| 39 | const run = useCallback( | |
| 40 | (work: () => Promise<unknown> | undefined, waits = true) => { | |
| 41 | const pressed = { status, at: Date.now() }; | |
| 42 | setPress(pressed); | |
| 43 | const done = () => setPress((current) => (current === pressed ? null : current)); | |
| 44 | Promise.resolve() | |
| 45 | .then(work) | |
| 46 | .then( | |
| 47 | () => { | |
| 48 | if (!waits) done(); | |
| 49 | }, | |
| 50 | (error: unknown) => { | |
| 51 | done(); | |
| 52 | onError?.(error); | |
| 53 | }, | |
| 54 | ); | |
| 55 | }, | |
| 56 | [status, onError], | |
| 57 | ); | |
| 58 | return { busy, run }; | |
| 59 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.