feat: drop-in-first redesign — one server, detachable viewport (0.4.0)#1
Merged
Conversation
…t" model Bare `perchd` becomes the `npm run dev` drop-in; `perchd dev` is demoted to a deprecated alias. The foreground/background split collapses into one explicit `-d`/`--detach` flag over one lifecycle: the server always lives in the background and a terminal is just a detachable viewport onto its log stream. Ctrl-C therefore detaches rather than kills; `perchd stop` terminates. Docs only — the implementation has NOT landed yet. Do not merge or publish ahead of the code. - README: new hero, "Detach and switch. Attach and watch." section, Ctrl-C callout, updated command table, migration table for `perchd dev`/`switch`. - CHANGELOG: [Unreleased] describing the 0.4.0 target, flagged as unshipped. - CONTRIBUTING: document the core model and its invariant. - CLAUDE.md: new — architecture, invariants, non-obvious constraints. - package.json: description/keywords match the drop-in positioning.
A server that survives detach must write to a file, and dev servers strip colour when stdout isn't a TTY (verified with picocolors: isColorSupported=false to a file, true with FORCE_COLOR=1). Today's `perchd dev` inherits the real TTY, so colour works now — a naive "background + tail the logfile" would make the drop-in look plainer than the `npm run dev` it replaces, undercutting the familiarity bet drop-in-first rests on. - README: replace the keypress note with two honest caveats (no keypress UI; no faithful progress re-rendering), and clarify that the three example commands each attach, so you'd ^C between them. - CLAUDE.md: flag the TTY-fidelity decision as open, blocking implementation.
The multi-process group-teardown assertion from the startForeground test is preserved, retargeted at startServer.
cli.ts is now the only place attachment is decided, keeping runSwitch free of viewport concerns. Adds --detach, perchd attach, the deprecated dev alias, and the cwd-worktree-vs-picker rule for bare perchd.
…a crash runSwitch stops the old server and clears the state record BEFORE writing the new one. The poller settled on first sight, so an ordinary cross-terminal switch made an attached viewport print '<branch> exited' and exit 1 — guardrail #3 of the design (clean 'perch moved to <branch>') almost never fired. Adds a graceMs window (default 2.5s): when our server looks gone, keep polling; a record with a different pid appearing during the window wins and yields perch-moved. Verified with two real terminals, not just the harness. The old integration test faked the transition atomically (no null gap, old pid still alive) and passed against the broken code; it now drives a real runSwitch.
The viewport returns the moment the new state record lands, while runSwitch is still in its readiness wait — so asserting on a variable assigned from .then() read null. Deterministic failure on all four CI jobs; passed locally by luck of timing. Hold the promise and await it. Also documents the graceMs / stop_timeout limitation in CLAUDE.md.
irwansetiawan
marked this pull request as ready for review
July 9, 2026 14:56
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
perchd becomes one server with a detachable viewport (tmux-for-dev-servers). The server always runs in the background; "foreground" is just whether a terminal is attached to its log stream.
Detach and switch. Attach and watch.
perchdis now thenpm run devdrop-in — switch to the worktree you're in, then attach. Outside a worktree it shows the picker.-d/--detachflips any switch to background. One uniform, explicit axis.perchd stopis the only way to terminate. This is the linchpin: it's what makes switching safe.perchd attach [branch]— new. Attach to the running server from any terminal.perchd dev— deprecated alias for bareperchd, prints a hint.perchd switchretained, but now attaches by default.Docs for this landed first (
0a94a1d,74f9278); the implementation now backs them, so the branch is coherent and safe to merge as a unit.Notable implementation decisions
cli.tsis the only place attachment is decided.runSwitchnever attaches — that keepsswitch.tsfree of viewport concerns and avoids aswitch ↔ attachimport cycle.startForegroundis deleted. Its multi-process group-teardown test coverage was preserved, retargeted atstartServer.FORCE_COLORinjection (src/core/env.ts). A server that survives detach must write to a file, and dev servers strip colour when stdout isn't a TTY (verified: picocolors →isColorSupported=falseto a file,truewithFORCE_COLOR=1). Without this the drop-in would look plainer than thenpm run devit replaces. Full fidelity (in-place spinners) would need a pty — rejected, native dependency.ActiveServer.foregroundretired.readStatestrips the legacy key; 0.3.x records havelogPath: ""and can't be attached to (perchd restartfixes them).detached,perch-moved,stopped,server-exited(which clears the record if the pid is still ours).Verification
pnpm typecheckclean ·pnpm test114/114 passing (was 95) ·pnpm buildclean (40K).env,viewport(7),attach(6),attach-flowintegration (3), passthrough-on-switch, legacy-state migration.perchd -d→ server backgrounded,statusshows ACTIVE, HTTP 200.perchd attach→ SIGINT →▸ detached, exit 0, server still serving.perchd→ attaches, SIGINT → detaches, server survives;perchd stopthen frees the port.Breaking changes (0.4.0)
≤ 0.3.x)perchd devperchdperchd(background switch)perchd -dperchd switch X(background)perchd X -dperchd stopstatuslabelled(fg)A real bug caught during verification (and fixed)
Driving this with two actual terminals — rather than trusting the test harness — surfaced that guardrail #3 (
▸ perch moved to <branch>) almost never fired.runSwitchstops the old server and clears the state record before writing the new one, so the viewport's poller saw "our record, dead pid" and settled onserver-exited— printing▸ main exited.and exiting 1 on every ordinary switch.The integration test missed it because it faked the transition atomically: new record written with no null gap and the old server still alive. It passed against broken code.
Fixed with a
graceMswindow (default 2.5s): when our server looks gone, keep polling; a record with a different pid appearing during the window wins →perch-moved. The integration test now drives a realrunSwitchthrough the real gap. Confirmed by hand: the two-terminal repro now prints▸ perch moved to main.and exits 0.