-
Notifications
You must be signed in to change notification settings - Fork 0
Spec Pending Start Rows
Written 2026-07-25. Status: implemented and since extended; this page is a historical artifact, kept for the API findings and the decision trail. The shipped behavior has outgrown it in three ways: restarts get rows too, resolution is a shared predicate (resolvingServer) rather than cwd-presence, and pending state persists across the command unloading, all of which the "non-goals" below deferred. Spawn Flow is the authoritative description of what runs today.
Decided in conversation on 2026-07-25 after a Raycast-API feasibility review; the API findings below were verified against developers.raycast.com and @raycast/api@1.104.18 typedefs on that date.
When the dashboard spawns dev servers, each target immediately gets a synthetic "starting" row in the list: project name, a progress-ring icon counting toward the 15s watchdog, and a yellow Starting… tag. If the server binds its port, the synthetic row hands off to the real server row (selection follows, as it already does). If the watchdog fires, the row turns into a persistent red Failed row whose ActionPanel carries the remedies (View Startup Log, Open in Terminal, Copy Fix Command when a fix is known, Dismiss). The failure toast for starts goes away; a toast cannot be a workspace, its actions vanish with it, and it degrades to an actionless HUD when the Raycast window is closed. The success toast stays.
-
Restarts.
restart()keeps its current toast behavior. Its state machine is local to one async function; lifting it into shared state is a later phase, after the row machinery exists. -
Retry action on failed rows. Needs a re-entry path into the once-only spawn flow (
spawnFlowFiredref). Open in Terminal covers the need manually. Design it later. - Persistence across dashboard close. Pending state is in-memory, same as the toast today.
-
Menu bar changes. Menu-bar starts already launch the dashboard with
launchContext.spawn, so they get rows for free.
-
src/index.tsx— everything below lives here. Anchors as of 2026-07-25 (commitf46e67c); re-locate by symbol, not line number:-
SpawnFailure,SPAWN_FAILURE,copyFixAction(~715-745): the diagnosis vocabulary and the copy-fix action. Reuse as-is. -
diagnoseSpawnFailure(~750): log-offset diagnosis. Reuse as-is. -
SpawnLogView(~301): already reusable, three call sites. - Spawn state machine:
SpawnPhasetype (~700), flow effect (~887-1024, guarded byspawnFlowFired), watch effect (~1030-1070), watchdog timeout effect (~1082-1140). -
expecting: Map<cwd, { name, logStart }>built at ~1020: this is exactly the data a pending row needs. - Selection wiring:
selectedItemId/onSelectionChange(~865, ~1381-1382) with the comment explaining why selection is two-way (pin once, then let the user own it). - EmptyView guard (~1405): currently keys on
servers.length === 0 && !effectiveLoading. - Row rendering +
List.Sectiongrouping byprojectKey(~1431-1459); rows setid={String(server.pid)}. - Row action shortcuts already bound (14 of them, ~340-615). Free chords for the failed-row panel: ⌘D, ⌘G, ⌘⇧L, ⌘⇧T.
-
- Wiki: Spawn Flow (state machine + toast-copy table; update both when this ships), UI Conventions (toast copy budget), Portless (why the no-TTY sentence is matched exactly).
- Raycast API findings (verified 2026-07-25):
List.Itemhas no per-rowisLoadingand no animated icons;getProgressIcon(progress, color)from@raycast/utilsis the progress affordance and is documented with a List.Item example. Accessorytagrenders a colored pill:{ tag: { value: "Starting…", color: Color.Yellow } }.List.Itemids need only be unique.selectedItemIdbehavior when pointing at a not-yet-rendered id is undocumented. Native search filtering may reorder sections unlessfiltering={{ keepSectionOrder: true }}.
type PendingStart = {
name: string; // project display name, from the spawn target
logStart: number; // spawn-log byte offset, copied from the expecting map
startedAt: number; // Date.now() at spawn, drives the progress ring
status: "starting" | "failed";
reason: SpawnFailure | null; // set when failed and diagnosable, else null
};
// keyed by cwd
const [pendingStarts, setPendingStarts] = useState<Map<string, PendingStart>>(new Map());Separate state slice. Do not inject synthetic entries into the useCachedPromise servers array via optimisticUpdate: every kill/restart handler runs optimisticUpdate filters over that array typed as DevServer[], and a fake entry would flow through all of them.
- Row
idandkey:`starting:${cwd}`. Namespaced so it can never collide with pid ids (bare digit strings). - A dedicated
List.Section title="Starting"rendered before the server sections, only when it has visible rows. - Add
filtering={{ keepSectionOrder: true }}to theListso a search query cannot demote the section.
Visible pending rows are derived, not stored: a pending entry renders only while its cwd is absent from servers. Compute in render:
const visiblePending = [...pendingStarts].filter(([cwd]) => !servers.some((s) => s.cwd === cwd));This guarantees the synthetic row disappears in the same render frame that the real row (same servers array) appears, so there is no frame where both are missing and selection falls off. A separate cleanup effect deletes resolved entries from pendingStarts state after the fact; state cleanup timing is then cosmetic, not correctness-bearing. Selection onto the real row is already handled by the existing watch effect.
-
Starting: icon
getProgressIcon(min(elapsed / 15000, 1)), title = project name, accessory{ tag: { value: "Starting…", color: Color.Yellow } }. Elapsed needs a driver: run a 1ssetIntervaltick effect while any entry hasstatus === "starting"(the servers poll does not re-render on a fixed cadence;sameServersreturns the previous reference when nothing changed). -
Failed: icon
Icon.XMarkCircletintedColor.Red, accessory{ tag: { value: "Failed", color: Color.Red }, tooltip: reason ? SPAWN_FAILURE[reason].title : "Not detected after 15s" }. Subtitle:SPAWN_FAILURE[reason].titlewhen diagnosed, else omit. -
Failed row ActionPanel, in order: View Startup Log (push
SpawnLogView, ⌘L to match server rows), Copy Fix Command (only whenreasonhas afix; reuse the command string fromSPAWN_FAILURE), Open in Terminal (Action.OpenwithterminalApp, ⌘T to match server rows), Dismiss (⌘⇧D; deletes the entry frompendingStarts). Row-level shortcuts may mirror server-row chords because the two row types never need both meanings at once.
- On transition to
phase: "spawning"(whereexpectingis built), also populatependingStartsfrom the same data,status: "starting". - Watchdog effect: for each still-missing cwd, set
status: "failed"andreason: diagnoseSpawnFailure(cwd, logStart). Hide the toast entirely on failure (toast.hide()); the rows are the failure surface now. Move selection to the first failed row (starting:${cwd}). Delete the watchdog's toast-failure branch (title/message/actions);SPAWN_FAILUREandcopyFixActionstay, now consumed by rows. The restart path's toast usage is untouched. - Success path (watch effect): unchanged, still flips the toast to Success. The cleanup effect removes resolved pending entries.
- Starting a cwd that already has a failed pending row resets that entry to
status: "starting"with freshlogStart/startedAt.
Widen to servers.length === 0 && visiblePending.length === 0 && !effectiveLoading, else a cold-start spawn renders the empty state and the starting row simultaneously.
| Input | Required behavior |
|---|---|
| Multi-target start, some bind, some fail | Bound cwds hand off to real rows; watchdog marks only missing cwds failed, each with its own diagnosis. Toast hidden. |
| Same cwd started again while a failed row shows | Entry resets to starting (fresh logStart, startedAt); no duplicate row. |
| Server binds after the watchdog marked it failed | Handoff rule still applies: cwd appears in servers, so the failed row disappears and the real row shows. Cleanup effect removes the entry. |
| User dismisses a failed row | Entry deleted; if the list is now empty, EmptyView shows. |
| User types in search while a start is pending | Starting section keeps position (keepSectionOrder: true); synthetic rows are filterable by title like any row. |
| Spawn throws before detaching (no runnable script) | Existing per-target failure toast path at spawn time is unchanged; no pending entry is created for targets that never entered expecting. |
| Dashboard closed mid-spawn and reopened | Pending state is gone (non-goal); behavior identical to today. |
Automated: npx tsc --noEmit, npm run lint, npm run build must pass.
Manual, using disposable fixtures (recreate in ~/Downloads/devserver-tests/, never the Desktop; delete after):
-
1-portless-down/package.json: dev scriptecho 'Proxy is not running and no TTY is available for sudo.' && exit 1 -
2-portless-benign/package.json: dev scriptecho 'Proxy is not running.' && sleep 60 -
3-port-conflict/package.json: dev scriptecho 'Error: listen EADDRINUSE: address already in use :::3000' && exit 1 - Any real project for the happy path.
Run npm run dev, then start each from the dashboard.
-
npx tsc --noEmit,npm run lint,npm run buildall exit 0. - Starting any project renders a
Startingsection row with that project's name and a yellowStarting…tag within one render of confirming, while the animated toast still shows. - The progress icon visibly advances over the 15s window (at least three distinct states).
- Happy path: when the server binds, the synthetic row is gone, the real row is present, and selection sits on the real row. At no point are both absent (no selection jump to an unrelated row).
- Fixture 1 after 15s: row shows red
Failedtag, subtitlePortless proxy isn't running; ActionPanel contains View Startup Log, Copy Fix Command, Open in Terminal, Dismiss. Copy Fix Command puts exactlyportless service installon the clipboard. No failure toast is visible. - Fixture 2 after 15s: row fails with no portless subtitle (undiagnosed), tooltip
Not detected after 15s; no Copy Fix Command action. - Fixture 3 after 15s: subtitle
Port already in use; no Copy Fix Command action. - Multi-start of fixtures 1+3 plus a real project: real project binds and hands off; two failed rows remain, each with its own subtitle; selection lands on the first failed row.
- Dismiss removes the row; with nothing else running, EmptyView appears (not alongside any row).
- Starting fixture 1 again from its failed row's project resets the row to
Starting…(no duplicate). - Restart (⌘⇧R) on a healthy server behaves exactly as before this change, including its timeout toast.
- With a pending start visible, typing in the search bar does not move the Starting section below the server sections.
npx tsc --noEmit && npm run lint && npm run buildAll three exit 0 = pass for criterion 1. Criteria 2-12 are the manual checklist above; report each as PASS/FAIL individually.
- Work on the branch
pending-start-rows, created offmain(create it if absent). The toast release (commits throughf46e67c) passed store review on 2026-07-25 and merges shortly; main must stay clean so/pull-contributionscan reconcile the CHANGELOG there without conflict. Do not amend or revert published commits. Do not push. Checkpoint-commit on the branch after each working increment. - Do not touch the published entries in
CHANGELOG.md. Add a new unreleased{PR_MERGE_DATE}entry for this feature. - Do not modify
restart()beyond what already shipped, and do not add pending rows for restarts. - Do not inject synthetic entries into the
useCachedPromisedata or usemutate/optimisticUpdatefor pending state. - Do not add a
docs/folder to the extension repo; docs go toDevServers.wiki(update Spawn Flow's toast table and state machine, and UI Conventions, in the same working session). - No em dashes in any written output (comments, changelog, docs).
- If
selectedItemIdpinned to a just-inserted synthetic row does not take focus on a real device (undocumented API behavior), stop and report; do not build a workaround without discussion. - If the watch/watchdog effects' dependency arrays need restructuring beyond adding pending-row bookkeeping, stop and ask; they encode subtle races documented in their comments.
The executor has no access to the conversation that produced this spec. Verify each; if any fails, stop and reassess rather than adapt silently:
-
src/index.tsxstill containsSPAWN_FAILURE,copyFixAction,diagnoseSpawnFailure,SpawnLogView, and the three-effect spawn machine (spawnFlowFiredref, watch effect, watchdog effect). Locate by symbol; line anchors above are from commitf46e67c. - Whether the store PR containing the toast rework has merged yet (check whether
CHANGELOG.md's head entry still says{PR_MERGE_DATE}, or whethergit logshows a Pull contributions merge afterf46e67c). It passed review on 2026-07-25, so either state is expected. Not a blocker either way: work on thepending-start-rowsbranch and add this feature's changelog entry as a new unreleased section above the head entry. -
@raycast/utilsexportsgetProgressIconat the installed version. - Server rows still use
id={String(server.pid)}and sections are still grouped byprojectKeyin unsorted PID order.