feat(tabs): auto mode — watch the browser, propose scripts, hear the answer - #30
Merged
Conversation
…tory The one-shot handoff can't back an agent loop: the tab state freezes at the moment the user clicks, a pushed script arrives with no explanation, and the CLI never learns whether it was applied. This adds the missing half. - tabs serve: version every posted tab state, keep the full SnapshotPayload (groups/windows/the extension's rendered markdown), and long-poll on GET /tabs?since so a watcher wakes only when something moved. Adds the suggestion/decision round trip. GET /script and POST /tabs keep their exact old shapes, so extension builds in the wild are unaffected. - tabs-history.ts: append one *delta* per version (not a snapshot — 500 of those would be a 20 MB file), capped and 0600. It's the one new data-at-rest risk here, so: --no-history, TABBREW_TABS_HISTORY=0, and history --clear. - tabs watch: block until the tabs change, then print what moved plus the snapshot in the exact format the skill reads. - tabs suggest: propose a script with a REQUIRED --note and wait for the verdict. Separate from `push` because push is fire-and-forget and note-free; a suggestion the user didn't ask for has to explain itself, and the loop has to hear "no" and why. - tabs history: show (or wipe) that change log. SUMMARY_MAX drops 59 → 57: `tabs suggest <file>` is now the widest label, so two existing summaries were trimmed to keep help inside 80 columns. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The watch loop needs instructions of its own, and they contradict the interactive skill on purpose: `tabbrew-tabs` requires listing every DEL target in chat and getting a "yes" before emitting, while in auto mode the panel's Accept/Deny card IS the confirmation — asking again in a chat the user may not even be looking at just makes them approve the same thing twice. Two skills, not one skill with a mode. SKILL.auto.md is the first skill whose source of truth is this repo: it documents `tabs watch`/`tabs suggest`, which don't exist in tabbrew-api, so it must never be re-synced from there. Noted in skills.ts and CLAUDE.md. AgentTarget grows from one skill to a list (`skillNames` + `resolveSkillsDir(scope, name)`), and init loops it across install, uninstall, and dry-run. `--variant` still applies only to tabbrew-tabs — the loop's instructions don't get cheaper with more tabs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ong-poll Two things found by running this against a real browser: Bun.serve defaults to a 10-SECOND idleTimeout and kills any request that quiet — which is every long-poll this server exists to hold open. The symptom lied: the socket died mid-wait, the client's fetch rejected, and `tabs watch` reported "nothing is listening" about a server that was running fine. idleTimeout: 0; the handlers already cap their own wait. Both long-polling clients now confirm with /health before blaming the bridge, and otherwise re-poll with the time that's left — a dropped connection is not the same event as a dead server, and only one of them is worth telling the user about. Adds a `failed` decision for "the user accepted and Chrome refused". Reporting that as `accepted` tells a watching agent its plan landed when the tabs never moved, and it will build the next suggestion on a browser state that never existed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
VERSION is read from package.json at compile time, so this bump is what makes an installed `tabbrew update` see the auto-mode build. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Turns the one-shot
tabs pushhandoff into a loop an agent can actually run: theextension streams tab state as it changes, the agent proposes a script with a
plain-language note, and the user's Accept/Deny comes back over the same bridge.
Extension side is colevels/tabbrew#95 —
merge and release this first, it is inert until the extension speaks protocol 2.
Why
Three things made the existing flow impossible to loop on:
snapshot from the moment the user clicked the card.
{ script }.The user saw a DSL diff, not "จะปิด 6 แท็บ YouTube แล้วรวม github เป็นกลุ่ม Code".
pushprinted "✓ Sent" and exited, so a loop wouldre-suggest the thing the user just threw away, forever.
What's here
Bridge protocol v2 (
tabs-serve.ts) — every posted tab state gets a version andkeeps the full
SnapshotPayload(groups/windows/the extension's own renderedmarkdown, so the CLI never has to reimplement
renderSnapshot). Adds a long-pollGET /tabs?since, the suggestion/decision round trip, andGET /history.POST /tabsandGET /scriptkeep their protocol-1 shapes byte for byte.tabs watch— blocks until the tabs actually change, then prints what moved plusthe snapshot. A timeout prints nothing and exits 0, so a loop branches on empty
output rather than an exit code.
tabs suggest— a separate command frompush, not two flags on it:--noteisrequired (a suggestion nobody asked for has to explain itself, and only a required
flag makes that reliable), and it waits for the verdict. Always exits 0 — a Deny is
an answer, not a failure.
tabs history— a delta per version, not a snapshot (500 snapshots of 200 tabswould be a 20 MB file). This is the one place the CLI accumulates browsing history at
rest —
tabs.jsonis overwritten and only holds open tabs, this log remembers closedones — so it is
0600, capped atTABBREW_TABS_HISTORY_MAX(500), and switchable offvia
tabs serve --no-history/TABBREW_TABS_HISTORY=0/tabs history --clear.tabbrew-autoskill — the loop written down, installed byinitalongsidetabbrew-tabs. It is the first skill whose source of truth is this repo (itdocuments commands that don't exist in tabbrew-api, so it must never be re-synced from
there), and it deliberately contradicts
tabbrew-tabson one point: no in-chat DELconfirmation, because the panel's Accept card is the confirmation.
AgentTargetgrows from one skill to a list.
Found by running it against a real browser
Bun.servedefaults to a 10-secondidleTimeoutand killed every long-poll thisserver exists to hold open. The symptom lied: the socket died mid-wait and
tabs watchreported "nothing is listening" about a server that was running fine./healthbefore blaming the bridge — adropped connection and a dead server are different events and only one is worth
telling the user about.
faileddecision for "the user accepted and Chrome refused". Reporting thatas
acceptedtells a watching agent its plan landed when the tabs never moved.Verified
typecheck+test+buildgreen. End-to-end against a real Chrome with theextension branch loaded: auto uplink posting
source: autowith groups/windows/snapshot,
tabs watchunblocking on a real tab change and naming the closed tabs,Deny returning
{"decision":"denied","reason":"don't manage my youtube"}, Acceptreturning
{"decision":"accepted","opCount":1}with the tabs actually regrouped(Social 1 → 4) and the delta log recording
~3. History cap verified by driving 70posts at
--max 10(settles at 19 lines, stays0600), and--no-historywrites nofile at all.
🤖 Generated with Claude Code