Skip to content

Add bsk tab group: native tab-group management scoped to the Agent Window - #339

Open
glatinone wants to merge 2 commits into
Tencent:mainfrom
glatinone:feat/tab-groups-cli
Open

glatinone wants to merge 2 commits into
Tencent:mainfrom
glatinone:feat/tab-groups-cli

Conversation

@glatinone

Copy link
Copy Markdown

Summary

Adds bsk tab group create|update|list|ungroup, backed by chrome.tabGroups,
following the same Agent Window sandbox rule as tab close / tab select:
every tab (and, for update/ungroup, the group itself) must already live in
the requesting session's Agent Window.

bsk tab group create --session <id> --tab <id> [--tab <id> ...] [--title <t>] [--color <c>]
bsk tab group create --session <id> --tab <id> --group-id <existing-group-id>
bsk tab group update <group-id> --session <id> [--title <t>] [--color <c>] [--collapsed|--expand]
bsk tab group list --session <id>
bsk tab group ungroup <group-id> --session <id>

What changed

  • bsk-protocol: TabGroupColor + TabGroupCreate/Update/List/Ungroup
    params/results, four new Method variants classified for the
    pending-interrupt gate (create/update/ungroup mutate, list is a passive
    read).
  • CLI: bsk tab group {create,update,list,ungroup} subcommands with
    --json and human table output.
  • daemon: forwards the four new tool.* methods the same way the
    existing tab.* ones are forwarded — no new daemon-side logic needed.
  • extension: handlers using chrome.tabs.group/ungroup and
    chrome.tabGroups.{get,query,update,move}, gated through the existing
    session/sandbox checks (authoriseAgentTab/new authoriseAgentGroup).
  • skill docs: tabs-and-profiles.md documents the commands, and is
    explicit that --title should come from what the grouped tabs are
    actually about (read tab list --scope agent first), not a placeholder
    like "Group 1".

A real bug this surfaced

Manual end-to-end testing against a real, isolated Edge profile (not just
mocks) found that a freshly created group can land outside the target
window on some Chromium builds/hosts — most reliably reproduced here with
session start --no-focus. handleTabGroupCreate now:

  1. Passes createProperties.windowId on create.
  2. Verifies placement afterward; if wrong, tries chrome.tabGroups.move.
  3. If that's a no-op, falls back to moving each member tab individually via
    chrome.tabs.move (the mechanism that's reliably worked for every other
    tab-management tool in this sandbox model, focused or not).
  4. If none of that lands the tabs in the Agent Window, it ungroups them and
    returns a clear cdp_failed/group_window_mismatch error instead of a
    hollow "success" with an empty tab_ids.

Covered by new unit tests for both fallback paths, including the
all-attempts-failed → error + cleanup case.

Testing

  • cargo test -p bsk-protocol / -p bsk --lib: all pass (162 + 361 tests).
  • pnpm vitest run (apps/extension): all pass (150 files / 2297 tests; 76
    in tabs.test.ts, 11 new, covering the handlers including both fallback
    paths above).
  • cargo clippy / cargo fmt --check: clean (pre-existing warnings only,
    unrelated to this change).
  • Manual end-to-end dogfooding: built the CLI and an unpacked extension
    from this branch, loaded it into a throwaway Edge profile against an
    isolated bsk daemon, and drove real chrome.tabGroups state — verified
    create/list/update/ungroup against real tabs in both a focused and
    an unfocused Agent Window, plus the sandbox-rejection path for a tab
    outside the Agent Window.
  • Integration tests that spawn a daemon subprocess don't run cleanly in the
    sandboxed shell I had available (named-pipe/Job-Object restrictions
    unrelated to this change — same failures reproduce on a fresh,
    unmodified checkout in that same shell); the manual dogfooding above was
    the substitute for that coverage.

Known scope limit

DSH plugin native tools (packages/dsh-plugin-browserskill) are not
updated — this PR covers the bsk CLI only. Happy to follow up if that's
wanted.

@iuyo5678

Copy link
Copy Markdown
Collaborator

Thanks for adding native tab-group support. This looks useful for organizing multi-tab tasks and making the results easier for users to review and take over.

There are a few issues that need to be addressed before merging:

  1. Fix the TypeScript errors.

    group_window_mismatch is passed to rpcError but is missing from RpcErrorReason. The new test helper also has three places where a potentially undefined tab ID is used as a number. Please fix these and verify that the extension’s TypeScript check passes.

  2. Apply authorization checks to all affected group members.

    authoriseAgentGroup currently checks only the group’s window, whereas authoriseAgentTab also enforces remote-session ownership and cross-session borrow restrictions.

    This allows an unowned user tab inside the Agent Window to be affected through group operations, even though tab_select rejects direct control of that same tab. Adding an owned tab to an existing group can also rename a group containing unowned tabs.

    Please validate all affected members before update, ungroup, and adding to an existing group, reusing the existing tab authorization rules. Tests should cover unowned members, mixed-ownership groups, and borrow conflicts.

  3. Correct the cross-window recovery path and verify the complete result.

    In an isolated Chromium test, moving individual tabs across windows removed their group membership. Once the final member moved out, the original group disappeared. The current fallback nevertheless continues to update and query the old groupId, which then fails.

    The test double preserves membership during these moves, so it currently misses this behavior. If the per-tab fallback remains, please recreate the group after restoring the tabs and use the valid group ID, or implement another verified recovery strategy.

    The result checks also need to cover:

    • Every requested tab reaching the intended window and group, rather than checking only whether at least one succeeded.
    • Verification of ungrouping and restoration, without swallowing cleanup failures and then claiming cleanup succeeded.
    • Clear reporting of partial changes when full recovery is impossible.

    This test confirmed Chromium’s individual-tab move behavior; it did not reproduce the reported Edge 153 placement issue. Concrete reproduction steps and environment details for that issue would help validate the recovery path.

  4. Handle cancellation throughout group creation.

    The last cancellation check occurs before tabs.group. If cancellation arrives during that call, subsequent moves and metadata updates can still execute, and the handler can return success.

    Please check cancellation after asynchronous stages and before subsequent mutations, while handling any necessary recovery separately. Tests should cover cancellation during group creation and relocation.

Please also update the branch against the latest main and resolve the tabs-and-profiles.md conflict, preserving the shared-login-state guidance already on main.

The feature is worth continuing. These authorization, recovery, cancellation, and type-checking issues should be resolved before merge.

glatinone and others added 2 commits September 29, 2026 21:42
…ndow

Adds `bsk tab group create|update|list|ungroup`, backed by chrome.tabGroups,
following the same Agent Window sandbox rule as `tab close` / `tab select`:
every tab (and, for `update`/`ungroup`, the group itself) must already live
in the requesting session's Agent Window.

- bsk-protocol: TabGroupColor + TabGroupCreate/Update/List/Ungroup
  params/results, four new Method variants classified for the
  pending-interrupt gate (create/update/ungroup mutate, list is a
  passive read).
- CLI: `bsk tab group {create,update,list,ungroup}` subcommands with
  --json and human table output.
- daemon: forwards the four new tool.* methods like the existing tab.*
  ones (no new daemon-side logic needed).
- extension: handlers using chrome.tabs.group/ungroup and
  chrome.tabGroups.{get,query,update,move}, gated through the existing
  session/sandbox checks (authoriseAgentTab/authoriseAgentGroup).

Real-browser testing (not just mocks) surfaced that a freshly created
group can land outside the target window on some Chromium builds/hosts
(most reliably reproduced with `session start --no-focus`, though the
underlying trigger turned out to be a stale unpacked-extension reload
during iteration, not focus itself). handleTabGroupCreate now verifies
placement after creation and falls back from a group-level move to
per-tab moves; if neither lands the tabs in the Agent Window, it
ungroups them and returns a clear error instead of a hollow success
with an empty tab_ids. Covered by new unit tests, including both
fallback paths.

Also documents (skill/references/tabs-and-profiles.md) that --title
should come from what the grouped tabs are actually about, not a
placeholder — read tab titles/URLs first, then name the group.

Testing:
- cargo test -p bsk-protocol / -p bsk --lib: all pass (162 + 361 tests)
- pnpm vitest run (apps/extension): all pass (150 files / 2297 tests,
  76 in tabs.test.ts covering the new handlers)
- cargo clippy / cargo fmt --check: clean (pre-existing warnings only)
- Manual end-to-end dogfooding against a real, isolated Edge profile
  (bsk CLI -> daemon -> unpacked extension -> real chrome.tabGroups):
  create/list/update/ungroup all verified against real tabs, in both
  focused and unfocused Agent Windows, plus the sandbox rejection path
  for a tab outside the Agent Window.

Known scope limit: DSH plugin native tools (packages/dsh-plugin-browserskill)
are not updated — this PR covers the bsk CLI only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Review follow-up on Tencent#339 (Zhang GH):

- Register `group_window_mismatch` in `RpcErrorReason` and fix the three
  potentially-undefined tab IDs in the test helper, so `tsc --noEmit`
  passes.
- Authorise every affected tab with the direct-control rules, not just
  the group's window: adding to an existing group, `update` and
  `ungroup` now validate all current members via `authoriseAgentTab`,
  so an unowned or borrowed tab can no longer be affected through group
  operations while `tab_select` would reject controlling it directly.
- Rebuild the cross-window recovery as a bounded ladder (group move,
  then per-tab moves plus an in-place group recreation), re-verifying
  placement from browser state at every step: per-tab moves across
  windows drop group membership and destroy the group in real Chromium,
  so the old group ID is no longer trusted. Failures report which tabs
  did not land and carry an honest `cleanup_state` instead of swallowing
  cleanup errors; the test double now models the membership loss.
- Check cancellation after each async stage of group creation and before
  subsequent mutations, recovering stranded memberships before stopping.
- Rebase onto latest main; resolve the tabs-and-profiles.md conflict
  preserving the shared-login-state guidance.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@glatinone

Copy link
Copy Markdown
Author

Thanks for the careful review — all four points are addressed in the new commits on the branch, which is also rebased onto latest main with the tabs-and-profiles.md conflict resolved (the shared-login-state guidance is preserved).

1. TypeScript errors — group_window_mismatch is now registered in RpcErrorReason (apps/extension/src/transport/types.ts), and the test helper normalises opts.tabIds into a validated number[] instead of using potentially-undefined values as tab IDs. tsc --noEmit is clean.

2. Authorization for all affected members — added authoriseAgentTabIds / authoriseAgentGroupMembers, which reuse the existing authoriseAgentTab rules against every affected tab. tab_group_update and tab_group_ungroup now authorise all current members before mutating, and tab_group_create with group_id authorises both the named tabs and the group's existing members. A group containing an unowned member or another session's borrowed tab is rejected outright, so a tab tab_select would refuse to control can no longer be renamed, regrouped or ungrouped through group operations. New tests cover unowned members (remote session), borrow conflicts, and the mixed case across create/update/ungroup.

3. Cross-window recovery — rebuilt as a bounded ladder in placeNewGroupInAgentWindow that re-verifies placement from browser state at every step: (1) verify after tabs.group; (2) tabGroups.move, re-verify; (3) per-tab moves, re-verify each tab landed, then recreate the group in the Agent Window and use the new ID — matching the Chromium behavior you verified, where a per-tab cross-window move drops group membership and the group disappears once the last member leaves. The old group ID is never trusted after per-tab moves. Final verification now requires every requested tab to be a member of the reported group, not just one; cleanup is no longer swallowed — cleanup_state: complete|failed is attached to the error — and partial placements name the tabs that did not land. The test double now models the membership loss and group destruction, so the recreation path is actually exercised.

Reproduction details for the original placement issue: I re-ran the flow end-to-end against a real Edge 153.0.4234.48 (Chromium 153) on Windows 11 with an unpacked MV3 extension and session start --no-focus — the same setup where you saw fresh groups land outside the Agent Window. In this run tab group create landed the group inside the Agent Window and the result was verified against browser state before success was reported; group list (scoped to the Agent Window) confirmed membership, and update (rename + collapse), adding a third tab via --group-id, and ungroup all behaved, with tabs left open after ungrouping. I could not force a live browser to misplace the group deterministically — that is exactly why the ladder verifies after every step instead of trusting any single placement — so the mismatch path is covered by the unit tests modelling your Chromium findings rather than by a live repro.

4. Cancellation — group creation checks the abort signal after each asynchronous stage and before each subsequent mutation: after tabs.group, at the start of and inside the relocation ladder, before re-grouping, and before metadata updates. When cancellation arrives mid-recovery, stranded memberships are cleaned up first and the cancelled error reports cleanup_state. New tests cover cancellation during tabs.group and during relocation.

Happy to iterate further on any of this.

@glatinone

Copy link
Copy Markdown
Author

Thanks for the review — all four points are addressed in 00dbd3c4 (member authz on update/ungroup/add, verified recovery path, cancellation between stages, and the RpcErrorReason fix). Branch is rebased on latest main and the tabs-and-profiles.md conflict is resolved. The scan pipeline is green and it merges cleanly. Would you have a moment to re-review? Happy to adjust anything.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants