fix(cli): add bringToFront command + tab list --full untruncated URLs (issue #19)
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.
This commit is contained in:
+50
-3
@@ -408,6 +408,12 @@ fn parse_command_inner(args: &[String], flags: &Flags) -> Result<Value, ParseErr
|
||||
"back" => 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<Value, ParseErr
|
||||
// `tabs` (plural) is a natural guess for the `tab` subcommand tree —
|
||||
// alias it so `tabs` / `tabs list` / `tabs new` all work (issue #8.4).
|
||||
"tab" | "tabs" => {
|
||||
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<Value, ParseErr
|
||||
}
|
||||
Ok(cmd)
|
||||
}
|
||||
Some("list") => 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<Value, ParseErr
|
||||
"action": "tab_switch",
|
||||
"tabId": tab_ref,
|
||||
})),
|
||||
None => 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"] {
|
||||
|
||||
@@ -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<Value, Stri
|
||||
// Phase 5 handlers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async fn handle_tab_list(state: &DaemonState) -> Result<Value, String> {
|
||||
async fn handle_tab_list(cmd: &Value, state: &DaemonState) -> Result<Value, String> {
|
||||
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<Value, String> {
|
||||
|
||||
+10
-2
@@ -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("→")
|
||||
|
||||
@@ -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 @ <url>`, `screenshot @ <url>`, `network @ <url>`.
|
||||
|
||||
Reference in New Issue
Block a user