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" })),
|
"back" => Ok(json!({ "id": id, "action": "back" })),
|
||||||
"forward" => Ok(json!({ "id": id, "action": "forward" })),
|
"forward" => Ok(json!({ "id": id, "action": "forward" })),
|
||||||
"reload" => Ok(json!({ "id": id, "action": "reload" })),
|
"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 ===
|
// === Core Actions ===
|
||||||
"click" => {
|
"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 —
|
// `tabs` (plural) is a natural guess for the `tab` subcommand tree —
|
||||||
// alias it so `tabs` / `tabs list` / `tabs new` all work (issue #8.4).
|
// alias it so `tabs` / `tabs list` / `tabs new` all work (issue #8.4).
|
||||||
"tab" | "tabs" => {
|
"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") => {
|
Some("new") => {
|
||||||
// Accepted forms:
|
// Accepted forms:
|
||||||
// tab new [url]
|
// tab new [url]
|
||||||
@@ -1528,7 +1539,13 @@ fn parse_command_inner(args: &[String], flags: &Flags) -> Result<Value, ParseErr
|
|||||||
}
|
}
|
||||||
Ok(cmd)
|
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") => {
|
Some("close") => {
|
||||||
let mut cmd = json!({ "id": id, "action": "tab_close" });
|
let mut cmd = json!({ "id": id, "action": "tab_close" });
|
||||||
if let Some(tab_ref) = rest.get(1) {
|
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",
|
"action": "tab_switch",
|
||||||
"tabId": tab_ref,
|
"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]
|
#[test]
|
||||||
fn test_get_text_hyphen_and_underscore_aliases() {
|
fn test_get_text_hyphen_and_underscore_aliases() {
|
||||||
for verb in ["get-text", "get_text"] {
|
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_stop" => handle_recording_stop(state).await,
|
||||||
"recording_restart" => handle_recording_restart(cmd, state).await,
|
"recording_restart" => handle_recording_restart(cmd, state).await,
|
||||||
"pdf" => handle_pdf(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_new" => handle_tab_new(cmd, state).await,
|
||||||
"tab_switch" => handle_tab_switch(cmd, state).await,
|
"tab_switch" => handle_tab_switch(cmd, state).await,
|
||||||
"tab_close" => handle_tab_close(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
|
// 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 mgr = state.browser.as_ref().ok_or("Browser not launched")?;
|
||||||
let tabs = mgr.tab_list();
|
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> {
|
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
|
// Tabs
|
||||||
if let Some(tabs) = data.get("tabs").and_then(|v| v.as_array()) {
|
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 {
|
for tab in tabs {
|
||||||
let tab_id = tab.get("tabId").and_then(|v| v.as_str()).unwrap_or("?");
|
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());
|
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 title = title.as_str();
|
||||||
let url = tab.get("url").and_then(|v| v.as_str()).unwrap_or("");
|
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
|
// Truncate very long URLs (e.g. multi-KB JWT/OTP login links) so
|
||||||
// the list stays readable instead of flooding the terminal.
|
// the list stays readable instead of flooding the terminal —
|
||||||
let url = truncate_middle(url, 120);
|
// 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 active = tab.get("active").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||||
let marker = if active {
|
let marker = if active {
|
||||||
color::cyan("→")
|
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*
|
replaces the old silent behaviour where the command ran on some *other*
|
||||||
tab and returned wrong data.
|
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**
|
**Reads landing on the wrong page**
|
||||||
`eval`, `screenshot`, and `network requests` print the page they ran
|
`eval`, `screenshot`, and `network requests` print the page they ran
|
||||||
against to stderr: `eval @ <url>`, `screenshot @ <url>`, `network @ <url>`.
|
against to stderr: `eval @ <url>`, `screenshot @ <url>`, `network @ <url>`.
|
||||||
|
|||||||
Reference in New Issue
Block a user