feat(tabs): fall back to port 49228, and prove the bridge is ours - #35
Merged
Conversation
`tabs serve` now takes the first free port of 49227/49228 and `tabs suggest` finds whichever is answering, so neither end has to be told which one is in use. A second port is only safe with an identity check. With one fixed port, guessing wrong was survivable — the request failed and the user was told nothing was listening. With a fallback, a foreign JSON service squatting on 49228 would otherwise be treated as the bridge, and `tabs suggest` would hand it a script describing the user's tabs. So `GET /health` now returns `service: "tabbrew-bridge"`, and `src/bridge.ts` skips anything that answers without proving what it is. Bridges already in the wild predate the marker, hence the fallback shape test (`ok: true` plus a numeric protocol/tabsVersion). The extension implements the identical predicate; the two must not drift. A busy port is diagnosed rather than merely skipped. On EADDRINUSE, `serve` probes who holds it: a stranger means step to the fallback, but another TabBrew bridge means refuse to start. A second bridge would be a silent dead end — Chrome takes the lowest port that answers, so the new one would sit there receiving nothing while the user waited. That refusal is also what keeps the single `outPath` state file to one writer. Wording follows the extension: the sidepanel card is "Connect to TabBrew CLI" and there is no Auto mode toggle any more — the screen itself is the switch. Every instruction telling users to find that toggle would now send them looking for something that doesn't exist. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merged
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.
Consumer half of colevels/tabbrew#99. Merge that one first — this repo tells
users to click a button and leave a screen open that ships over there.
Port fallback
config.serve.portbecomesconfig.serve.ports = [49227, 49228].tabs servebinds the first free one;
tabs suggestconnects to the first that answers.Neither end has to be told which is in use.
Still no
--portflag: the extension lists exactly these two in both manifests'optional_host_permissions, so a bridge anywhere else is invisible to Chrome nomatter what is listening. The list is a cross-repo contract — changing it means
changing this array, both manifests, and the extension's
BRIDGE_PORTStogether.
TABBREW_SERVE_PORTnow pins a single port, for tests.Identity, not just reachability (
src/bridge.ts)This is the part that makes a second port safe. With one fixed port, guessing
wrong was survivable. With a fallback, a foreign JSON service squatting on 49228
would be treated as the bridge — and
tabs suggestwould hand it a scriptdescribing the user's tabs.
GET /healthnow carriesservice: "tabbrew-bridge". Never rename or dropthat field. Bridges already in the wild predate it, so
looksLikeBridgealsoaccepts
ok: trueplus a numericprotocol/tabsVersion— a shape a generic{"ok":true}health endpoint does not have. The extension implements the samepredicate; if the two drift, one end adopts a service the other rejects.
Probing is sequential in preference order at both ends, with a 1.5s timeout —
with two bridges up, "the lowest port" is stable where "first to reply" is a
race that could point the CLI at one bridge while Chrome talks to the other.
A busy port is diagnosed, not just skipped
On
EADDRINUSE,serveasks who holds it:Starting a second bridge would be a silent dead end: Chrome takes the lowest
port that answers, so the new one would receive nothing while the user waited
for tabs that never arrived. The refusal is also what keeps the single
outPathstate file to one writer — two bridges would otherwise clobber eachother's
tabs.json.Wording
Every instruction that said click Send to Claude Code and switch Auto
mode on would now send users hunting for a toggle that no longer exists. All
of them become click Connect to TabBrew CLI and leave that screen open:
tabs-list(3 strings),tabs-servenext-steps,awareness.ts,tabbrew-script/SKILL.md,README.md,CLAUDE.md, and theTABBREW_SERVE_PORThelp line.Verification
bun run typecheck,bun test(20/20),bun run buildall clean.49227 — one without the
servicemarker — and the newserveidentified itcorrectly through the legacy shape path, which is exactly the backward-compat
case:
{ok:true}squatter anda grafana-ish body both rejected; marker bridge and legacy bridge both
accepted; dead port rejected; and
discoverBridgeskips a squatter on the lowport to find the real bridge on the high one.
a real bridge for the duration. The peer path and the predicate that drives
both are covered.
🤖 Generated with Claude Code