From 1ea6b1a2c58619b9d8d3865220734272fe6f4ef1 Mon Sep 17 00:00:00 2001 From: leeguooooo Date: Sat, 13 Jun 2026 15:44:25 +0900 Subject: [PATCH] fix(cli): add bringToFront command + tab list --full untruncated URLs (issue #19) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two SPA-SSO debugging gaps: - The core skill referenced `bringToFront` but the CLI parser never mapped it (the daemon handler existed) → 'Unknown command'. Wire up bringToFront / bring-to-front / bringtofront → the existing action. - 'stale sessionId — re-open your target URL' recovery was impossible because `tab list` truncates long URLs with '…', cutting client_id/state out of SSO links. Add `tab list --full` (also `tab --full`) to print untruncated URLs; SKILL.md documents the recovery (full URL + re-open the stable entry URL). The stale-session itself auto-recovers via the stable per-tab relay session id (#17, extension 0.4.4). Parse tests for both new forms; verified live. --- cli/src/commands.rs | 53 ++++++++++++++++++++++++++++++++++++--- cli/src/native/actions.rs | 11 +++++--- cli/src/output.rs | 12 +++++++-- skill-data/core/SKILL.md | 7 ++++++ 4 files changed, 75 insertions(+), 8 deletions(-) diff --git a/cli/src/commands.rs b/cli/src/commands.rs index f73b735..5c1bd50 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -408,6 +408,12 @@ fn parse_command_inner(args: &[String], flags: &Flags) -> Result Ok(json!({ "id": id, "action": "back" })), "forward" => Ok(json!({ "id": id, "action": "forward" })), "reload" => Ok(json!({ "id": id, "action": "reload" })), + // Explicit opt-in to raise the active tab to the foreground (the core + // skill references it; the daemon handler existed but the CLI didn't map + // it — issue #19). Accept the documented camelCase + kebab/lowercase. + "bringToFront" | "bring-to-front" | "bringtofront" => { + Ok(json!({ "id": id, "action": "bringtofront" })) + } // === Core Actions === "click" => { @@ -1496,7 +1502,12 @@ fn parse_command_inner(args: &[String], flags: &Flags) -> Result { - match rest.first().copied() { + // `--full` makes `tab list` emit untruncated URLs (needed to re-open + // a long SSO/redirect URL after a stale session — issue #19). Pick + // the subcommand as the first non-flag arg so the flag can appear + // anywhere (`tab --full`, `tab list --full`). + let full = rest.contains(&"--full"); + match rest.iter().find(|a| !a.starts_with("--")).copied() { Some("new") => { // Accepted forms: // tab new [url] @@ -1528,7 +1539,13 @@ fn parse_command_inner(args: &[String], flags: &Flags) -> Result Ok(json!({ "id": id, "action": "tab_list" })), + Some("list") => { + let mut cmd = json!({ "id": id, "action": "tab_list" }); + if full { + cmd["full"] = json!(true); + } + Ok(cmd) + } Some("close") => { let mut cmd = json!({ "id": id, "action": "tab_close" }); if let Some(tab_ref) = rest.get(1) { @@ -1541,7 +1558,13 @@ fn parse_command_inner(args: &[String], flags: &Flags) -> Result Ok(json!({ "id": id, "action": "tab_list" })), + None => { + let mut cmd = json!({ "id": id, "action": "tab_list" }); + if full { + cmd["full"] = json!(true); + } + Ok(cmd) + } } } @@ -3716,6 +3739,30 @@ mod tests { ); } + #[test] + fn test_tab_list_full_flag() { + // issue #19: `--full` → untruncated URLs; works as `tab list --full`, + // `tab --full`, and `tabs --full`. Plain list has no `full`. + for inv in ["tab list --full", "tab --full", "tabs --full"] { + let cmd = parse_command(&args(inv), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "tab_list", "{inv}"); + assert_eq!(cmd["full"], true, "{inv}"); + } + let plain = parse_command(&args("tab list"), &default_flags()).unwrap(); + assert_eq!(plain["action"], "tab_list"); + assert!(plain.get("full").is_none()); + } + + #[test] + fn test_bring_to_front_aliases() { + // issue #19: the documented `bringToFront` (+ kebab/lowercase) maps to + // the existing daemon action. + for inv in ["bringToFront", "bring-to-front", "bringtofront"] { + let cmd = parse_command(&args(inv), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "bringtofront", "{inv}"); + } + } + #[test] fn test_get_text_hyphen_and_underscore_aliases() { for verb in ["get-text", "get_text"] { diff --git a/cli/src/native/actions.rs b/cli/src/native/actions.rs index 411341c..09c05ea 100644 --- a/cli/src/native/actions.rs +++ b/cli/src/native/actions.rs @@ -1364,7 +1364,7 @@ pub async fn execute_command(cmd: &Value, state: &mut DaemonState) -> Value { "recording_stop" => handle_recording_stop(state).await, "recording_restart" => handle_recording_restart(cmd, state).await, "pdf" => handle_pdf(cmd, state).await, - "tab_list" => handle_tab_list(state).await, + "tab_list" => handle_tab_list(cmd, state).await, "tab_new" => handle_tab_new(cmd, state).await, "tab_switch" => handle_tab_switch(cmd, state).await, "tab_close" => handle_tab_close(cmd, state).await, @@ -4355,10 +4355,15 @@ async fn handle_keyboard(cmd: &Value, state: &DaemonState) -> Result Result { +async fn handle_tab_list(cmd: &Value, state: &DaemonState) -> Result { let mgr = state.browser.as_ref().ok_or("Browser not launched")?; let tabs = mgr.tab_list(); - Ok(json!({ "tabs": tabs })) + // Echo `full` so the formatter prints untruncated URLs (issue #19). + if cmd.get("full").and_then(|v| v.as_bool()).unwrap_or(false) { + Ok(json!({ "tabs": tabs, "full": true })) + } else { + Ok(json!({ "tabs": tabs })) + } } async fn handle_tab_new(cmd: &Value, state: &mut DaemonState) -> Result { diff --git a/cli/src/output.rs b/cli/src/output.rs index 0ef4c7e..b274405 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -477,6 +477,9 @@ pub fn print_response_with_opts(resp: &Response, action: Option<&str>, opts: &Ou } // Tabs if let Some(tabs) = data.get("tabs").and_then(|v| v.as_array()) { + // `tab list --full` prints untruncated URLs so a long SSO/redirect + // URL can actually be re-opened after a stale session (issue #19). + let full = data.get("full").and_then(|v| v.as_bool()).unwrap_or(false); for tab in tabs { let tab_id = tab.get("tabId").and_then(|v| v.as_str()).unwrap_or("?"); let tab_label = tab.get("label").and_then(|v| v.as_str()); @@ -491,8 +494,13 @@ pub fn print_response_with_opts(resp: &Response, action: Option<&str>, opts: &Ou let title = title.as_str(); let url = tab.get("url").and_then(|v| v.as_str()).unwrap_or(""); // Truncate very long URLs (e.g. multi-KB JWT/OTP login links) so - // the list stays readable instead of flooding the terminal. - let url = truncate_middle(url, 120); + // the list stays readable instead of flooding the terminal — + // unless `--full` was asked for (to re-open the exact URL). + let url = if full { + url.to_string() + } else { + truncate_middle(url, 120) + }; let active = tab.get("active").and_then(|v| v.as_bool()).unwrap_or(false); let marker = if active { color::cyan("→") diff --git a/skill-data/core/SKILL.md b/skill-data/core/SKILL.md index 60290b2..b02bb19 100644 --- a/skill-data/core/SKILL.md +++ b/skill-data/core/SKILL.md @@ -610,6 +610,13 @@ forbids debugging). The session no longer has a live tab — re-run replaces the old silent behaviour where the command ran on some *other* tab and returned wrong data. +To recover, you need the tab's **exact** URL (query params and all — a long +SSO/redirect link breaks if truncated). `tab list` shortens long URLs with +`…`; use **`tab list --full`** to print them untruncated, then re-`open` the +right one. For multi-redirect SSO flows, re-open the **stable entry URL** +(not the mid-redirect one) and `wait` a few seconds for the SPA to settle +before snapshotting. + **Reads landing on the wrong page** `eval`, `screenshot`, and `network requests` print the page they ran against to stderr: `eval @ `, `screenshot @ `, `network @ `.