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:
@@ -2531,6 +2531,20 @@ async fn handle_navigate(cmd: &Value, state: &mut DaemonState) -> Result<Value,
|
||||
state.ref_map.clear();
|
||||
state.iframe_sessions.clear();
|
||||
state.active_frame_id = None;
|
||||
|
||||
// `--reuse-tab`: if a tab already shows this URL (same origin+path), switch
|
||||
// to it instead of navigating — preserves any in-page state and stops
|
||||
// re-`open` from piling up duplicate tabs on rebind (issue #21).
|
||||
if cmd
|
||||
.get("reuseTab")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
if let Ok(Some(switched)) = mgr.reuse_tab_for_url(url).await {
|
||||
return Ok(switched);
|
||||
}
|
||||
}
|
||||
|
||||
let result = mgr.navigate(url, wait_until).await?;
|
||||
// Adaptive humanize: sample the freshly loaded page for known behavioural
|
||||
// anti-bot vendors and escalate this session to Human if any are present.
|
||||
@@ -4355,8 +4369,12 @@ async fn handle_keyboard(cmd: &Value, state: &DaemonState) -> Result<Value, Stri
|
||||
// Phase 5 handlers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async fn handle_tab_list(cmd: &Value, state: &DaemonState) -> Result<Value, String> {
|
||||
let mgr = state.browser.as_ref().ok_or("Browser not launched")?;
|
||||
async fn handle_tab_list(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {
|
||||
let mgr = state.browser.as_mut().ok_or("Browser not launched")?;
|
||||
// Re-sync with the live browser so the list reflects tabs opened by other
|
||||
// sessions or re-attached after a cross-process nav, and drops gone ones
|
||||
// (issue #21). Best-effort: a stale list still beats erroring the command.
|
||||
mgr.resync_targets().await.ok();
|
||||
let tabs = mgr.tab_list();
|
||||
// Echo `full` so the formatter prints untruncated URLs (issue #19).
|
||||
if cmd.get("full").and_then(|v| v.as_bool()).unwrap_or(false) {
|
||||
@@ -4394,9 +4412,20 @@ async fn handle_tab_switch(cmd: &Value, state: &mut DaemonState) -> Result<Value
|
||||
let tab_ref_str = cmd
|
||||
.get("tabId")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("Missing 'tabId' parameter (expected `t<N>` or a label)")?;
|
||||
let tab_ref = super::browser::TabRef::parse(tab_ref_str)?;
|
||||
let tab_id = mgr.resolve_tab_ref(&tab_ref)?;
|
||||
.ok_or("Missing 'tabId' parameter (expected `t<N>`, a label, or a targetId)")?;
|
||||
// Re-sync first so a tab opened by another session, or one that re-attached
|
||||
// after a cross-process nav, is adoptable from here (issue #21).
|
||||
mgr.resync_targets().await.ok();
|
||||
// A CDP `targetId` (shown in `tab list`) is stable across sessions, so accept
|
||||
// it directly for adopting a specific pre-existing tab — falling back to the
|
||||
// per-session `t<N>` / label form.
|
||||
let tab_id = match mgr.tab_id_for_target(tab_ref_str) {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
let tab_ref = super::browser::TabRef::parse(tab_ref_str)?;
|
||||
mgr.resolve_tab_ref(&tab_ref)?
|
||||
}
|
||||
};
|
||||
state.ref_map.clear();
|
||||
state.iframe_sessions.clear();
|
||||
state.active_frame_id = None;
|
||||
|
||||
Reference in New Issue
Block a user