feat(tabs): live tab resync + adopt-by-targetId + open --reuse-tab (#21)

Multi-session over one relayed Chrome had a tab-identity fracture: each daemon
discovered targets ONCE at connect and assigned its own t<N> indices, so a tab
filled in session A was unreachable from session B — B saw a disjoint/blank set
and rebinding via 'open' piled up duplicate tabs. A stranded, still-filled tab
could not be finished from any other session.

- 'tab list' now re-syncs the live target set on every call: adopts tabs other
  sessions opened (or that re-attached after a cross-process nav), drops gone
  ones (clears phantom rows), and refreshes url/title from each live tab via
  Target.getTargetInfo (the relay only stamps target_info on attach, so it goes
  stale/blank after navigation — which made rows indistinguishable).
- 'tab list --full' now prints each tab's stable CDP targetId. Unlike t<N>
  (per-session, reassigned each connect), targetId is stable across every session
  on the relayed Chrome.
- 'tab <targetId>' adopts a specific pre-existing tab — including another
  session's — WITHOUT reloading, so a half-filled form survives. handle_tab_switch
  resyncs first, then resolves a raw targetId before falling back to t<N>/label.
- 'open <url> --reuse-tab' (alias --reuse) switches to an existing tab already on
  that URL (matched by origin+path, ignoring volatile query/fragment) instead of
  spawning a duplicate.

Verified live over the extension relay: a fresh session's 'tab list --full' lists
the user's real tabs with correct titles + full URLs + targetIds, and
'tab <targetId>' lands on and reads the exact stranded Rakuten account-recovery
form from the report. Unit tests cover URL normalization + --reuse-tab parsing;
full suite green. Docs: --help Tabs section + core skill multi-session guidance.
This commit is contained in:
leeguooooo
2026-06-13 17:11:38 +09:00
parent 6b9de10c73
commit c7de19b099
5 changed files with 279 additions and 13 deletions
+24
View File
@@ -370,6 +370,12 @@ fn parse_command_inner(args: &[String], flags: &Flags) -> Result<Value, ParseErr
if flags.provider.is_some() {
nav_cmd["waitUntil"] = json!("none");
}
// `--reuse-tab`: adopt an existing tab already on this URL instead of
// navigating/spawning a new one (issue #21 — avoids duplicate tabs on
// rebind, preserves in-page state).
if rest.iter().any(|a| *a == "--reuse-tab" || *a == "--reuse") {
nav_cmd["reuseTab"] = json!(true);
}
// Explicit readiness override (issue #10): SPAs whose `load` event
// never fires (a long-lived XHR/websocket holds it open) hang out the
// load-event wait. `--wait-until domcontentloaded` returns as soon as
@@ -3575,6 +3581,24 @@ mod tests {
assert_eq!(cmd["url"], "https://example.com");
}
#[test]
fn test_navigate_reuse_tab_flag() {
let cmd = parse_command(
&args("open https://example.com --reuse-tab"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "navigate");
assert_eq!(cmd["reuseTab"], true);
// Alias.
let cmd2 =
parse_command(&args("open https://example.com --reuse"), &default_flags()).unwrap();
assert_eq!(cmd2["reuseTab"], true);
// Absent by default.
let cmd3 = parse_command(&args("open https://example.com"), &default_flags()).unwrap();
assert!(cmd3.get("reuseTab").is_none());
}
#[test]
fn test_navigate_with_headers() {
let mut flags = default_flags();