mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
feat(session): support unfocused agent windows (--no-focus) (#87)
`bsk session start --no-focus` opens the Agent Window in the background without stealing focus. The flag threads focused=false through CLI -> daemon IPC -> tool.session_start -> chrome.windows.create; an omitted `focused` keeps the extension default (true) so older clients are unaffected. Reworked onto current main so it composes with #52 window sizing: AgentWindowApi.create / SessionManager.start (extension), start_session (CLI) and the daemon's session start now take options objects/structs ({ size, focused }) instead of colliding positional params. Co-authored-by: alectimison-maker <alec.timison@gmail.com>
This commit is contained in:
co-authored by
alectimison-maker
parent
0ce497f017
commit
f443cfae74
@@ -44,3 +44,53 @@ describe("chromeAgentWindowApi.ensureActiveTab", () => {
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("chromeAgentWindowApi.create", () => {
|
||||
const create = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal("chrome", {
|
||||
windows: { create },
|
||||
});
|
||||
create.mockReset();
|
||||
create.mockResolvedValue({ id: 100 });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("focuses Agent Windows by default", async () => {
|
||||
await chromeAgentWindowApi.create(AGENT_WINDOW_HOME);
|
||||
|
||||
expect(create).toHaveBeenCalledWith({
|
||||
type: "normal",
|
||||
focused: true,
|
||||
url: AGENT_WINDOW_HOME,
|
||||
});
|
||||
});
|
||||
|
||||
it("can create an Agent Window without stealing focus", async () => {
|
||||
await chromeAgentWindowApi.create(AGENT_WINDOW_HOME, { focused: false });
|
||||
|
||||
expect(create).toHaveBeenCalledWith({
|
||||
type: "normal",
|
||||
focused: false,
|
||||
url: AGENT_WINDOW_HOME,
|
||||
});
|
||||
});
|
||||
|
||||
it("passes an optional window size through the options object", async () => {
|
||||
await chromeAgentWindowApi.create(AGENT_WINDOW_HOME, {
|
||||
size: { width: 1280, height: 800 },
|
||||
});
|
||||
|
||||
expect(create).toHaveBeenCalledWith({
|
||||
type: "normal",
|
||||
focused: true,
|
||||
url: AGENT_WINDOW_HOME,
|
||||
width: 1280,
|
||||
height: 800,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { AgentWindowApi } from "../agent-window";
|
||||
import type { AgentWindowApi, AgentWindowCreateOptions } from "../agent-window";
|
||||
import { SessionManager } from "../manager";
|
||||
|
||||
function fakeAgentWindow(): AgentWindowApi & {
|
||||
@@ -8,7 +8,7 @@ function fakeAgentWindow(): AgentWindowApi & {
|
||||
ensureActiveTabMock: ReturnType<typeof vi.fn>;
|
||||
} {
|
||||
let nextId = 100;
|
||||
const createMock = vi.fn(async (_url: string) => {
|
||||
const createMock = vi.fn(async (_url: string, _opts?: AgentWindowCreateOptions) => {
|
||||
const id = nextId++;
|
||||
return id;
|
||||
});
|
||||
@@ -30,7 +30,7 @@ describe("SessionManager", () => {
|
||||
const sm = new SessionManager({ agentWindow: aw, now: () => 1700000000000 });
|
||||
const ctx = await sm.start("aa11");
|
||||
expect(aw.createMock).toHaveBeenCalledOnce();
|
||||
expect(aw.createMock).toHaveBeenCalledWith("about:blank");
|
||||
expect(aw.createMock).toHaveBeenCalledWith("about:blank", {});
|
||||
expect(aw.ensureActiveTabMock).toHaveBeenCalledOnce();
|
||||
expect(aw.ensureActiveTabMock).toHaveBeenCalledWith(100, "about:blank");
|
||||
expect(ctx.sessionId).toBe("aa11");
|
||||
@@ -42,11 +42,22 @@ describe("SessionManager", () => {
|
||||
it("forwards an optional window size when starting a session", async () => {
|
||||
const aw = fakeAgentWindow();
|
||||
const sm = new SessionManager({ agentWindow: aw });
|
||||
const ctx = await sm.start("aa11", { width: 1280, height: 800 });
|
||||
expect(aw.createMock).toHaveBeenCalledWith("about:blank", { width: 1280, height: 800 });
|
||||
const ctx = await sm.start("aa11", { size: { width: 1280, height: 800 } });
|
||||
expect(aw.createMock).toHaveBeenCalledWith("about:blank", {
|
||||
size: { width: 1280, height: 800 },
|
||||
});
|
||||
expect(ctx.agentWindowId).toBe(100);
|
||||
});
|
||||
|
||||
it("forwards an explicit unfocused start to the Agent Window", async () => {
|
||||
const aw = fakeAgentWindow();
|
||||
const sm = new SessionManager({ agentWindow: aw });
|
||||
|
||||
await sm.start("aa11", { focused: false });
|
||||
|
||||
expect(aw.createMock).toHaveBeenCalledWith("about:blank", { focused: false });
|
||||
});
|
||||
|
||||
it("indexes the session by sessionId and agent window id", async () => {
|
||||
const aw = fakeAgentWindow();
|
||||
const sm = new SessionManager({ agentWindow: aw });
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
export interface AgentWindowApi {
|
||||
create(url: string, size?: { width: number; height: number }): Promise<number>;
|
||||
create(url: string, opts?: AgentWindowCreateOptions): Promise<number>;
|
||||
remove(windowId: number): Promise<void>;
|
||||
/**
|
||||
* Guarantee the Agent Window has an active, CDP-navigable tab.
|
||||
@@ -18,16 +18,24 @@ export interface AgentWindowApi {
|
||||
ensureActiveTab(windowId: number, url: string): Promise<void>;
|
||||
}
|
||||
|
||||
/** Creation hints for a new Agent Window. */
|
||||
export interface AgentWindowCreateOptions {
|
||||
/** Optional outer size in CSS pixels. */
|
||||
size?: { width: number; height: number };
|
||||
/** Defaults to true so existing sessions keep visible Agent Windows. */
|
||||
focused?: boolean;
|
||||
}
|
||||
|
||||
/** Initial tab URL for every new session's Agent Window. */
|
||||
export const AGENT_WINDOW_HOME = "about:blank";
|
||||
|
||||
export const chromeAgentWindowApi: AgentWindowApi = {
|
||||
async create(url: string, size?: { width: number; height: number }): Promise<number> {
|
||||
async create(url: string, opts: AgentWindowCreateOptions = {}): Promise<number> {
|
||||
const win = await chrome.windows.create({
|
||||
type: "normal",
|
||||
focused: true,
|
||||
focused: opts.focused ?? true,
|
||||
url,
|
||||
...(size ? { width: size.width, height: size.height } : {}),
|
||||
...(opts.size ? { width: opts.size.width, height: opts.size.height } : {}),
|
||||
});
|
||||
if (typeof win?.id !== "number") {
|
||||
throw new Error("[bh] chrome.windows.create returned no window id");
|
||||
|
||||
@@ -25,6 +25,14 @@ export interface SessionManagerOptions {
|
||||
now?: () => number;
|
||||
}
|
||||
|
||||
/** Options for starting a session's Agent Window. */
|
||||
export interface SessionStartOptions {
|
||||
/** Optional Agent Window outer size in CSS pixels. */
|
||||
size?: { width: number; height: number };
|
||||
/** Defaults to true so existing clients keep visible Agent Windows. */
|
||||
focused?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Owner of all live agent sessions inside the extension.
|
||||
*
|
||||
@@ -128,18 +136,11 @@ export class SessionManager {
|
||||
* Returns the created window id so callers can echo it back to the
|
||||
* daemon in the `tool.session_start` reply.
|
||||
*/
|
||||
async start(
|
||||
sessionId: string,
|
||||
size?: { width: number; height: number },
|
||||
): Promise<SessionContext> {
|
||||
async start(sessionId: string, opts: SessionStartOptions = {}): Promise<SessionContext> {
|
||||
if (this.sessions.has(sessionId)) {
|
||||
throw new Error(`[bh] session ${sessionId} already exists`);
|
||||
}
|
||||
// Only pass `size` when given so the no-size call shape (and its
|
||||
// chrome.windows.create payload) stays exactly as before.
|
||||
const windowId = size
|
||||
? await this.agentWindow.create(AGENT_WINDOW_HOME, size)
|
||||
: await this.agentWindow.create(AGENT_WINDOW_HOME);
|
||||
const windowId = await this.agentWindow.create(AGENT_WINDOW_HOME, opts);
|
||||
await this.agentWindow.ensureActiveTab(windowId, AGENT_WINDOW_HOME);
|
||||
const ctx: SessionContext = {
|
||||
sessionId,
|
||||
|
||||
@@ -69,6 +69,25 @@ describe("ToolDispatcher", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards an unfocused session start to the Agent Window", async () => {
|
||||
const { transport, deliver } = fakeTransport();
|
||||
const create = vi.fn(async () => 4242);
|
||||
const sessions = new SessionManager({
|
||||
agentWindow: {
|
||||
create,
|
||||
remove: vi.fn(),
|
||||
ensureActiveTab: vi.fn(async () => {}),
|
||||
},
|
||||
});
|
||||
const dispatcher = new ToolDispatcher({ transport, sessions });
|
||||
dispatcher.start();
|
||||
|
||||
deliver(makeRequest("tool.session_start", { session_id: "aa11", focused: false }));
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(create).toHaveBeenCalledWith("about:blank", { focused: false });
|
||||
});
|
||||
|
||||
it("routes tool.session_stop and replies with empty result", async () => {
|
||||
const { transport, sent, deliver } = fakeTransport();
|
||||
const sessions = new SessionManager({
|
||||
|
||||
@@ -107,7 +107,7 @@ describe("handleSessionStart window size", () => {
|
||||
const sm = new SessionManager({ agentWindow: aw });
|
||||
const result = await handleSessionStart(sm, { session_id: "aa11", width: 1280, height: 800 });
|
||||
expect(result).toEqual({ agent_window_id: 100 });
|
||||
expect(aw.create).toHaveBeenCalledWith("about:blank", { width: 1280, height: 800 });
|
||||
expect(aw.create).toHaveBeenCalledWith("about:blank", { size: { width: 1280, height: 800 } });
|
||||
});
|
||||
|
||||
it("creates the window without size when width/height are omitted", async () => {
|
||||
@@ -115,7 +115,15 @@ describe("handleSessionStart window size", () => {
|
||||
const sm = new SessionManager({ agentWindow: aw });
|
||||
const result = await handleSessionStart(sm, { session_id: "aa11" });
|
||||
expect(result).toEqual({ agent_window_id: 100 });
|
||||
expect(aw.create).toHaveBeenCalledWith("about:blank");
|
||||
expect(aw.create).toHaveBeenCalledWith("about:blank", {});
|
||||
});
|
||||
|
||||
it("forwards focused: false to the Agent Window", async () => {
|
||||
const aw = fakeAgentWindow([100]);
|
||||
const sm = new SessionManager({ agentWindow: aw });
|
||||
const result = await handleSessionStart(sm, { session_id: "aa11", focused: false });
|
||||
expect(result).toEqual({ agent_window_id: 100 });
|
||||
expect(aw.create).toHaveBeenCalledWith("about:blank", { focused: false });
|
||||
});
|
||||
|
||||
it("rejects a lone width without height", async () => {
|
||||
|
||||
@@ -50,6 +50,8 @@ export interface SessionStartParams {
|
||||
width?: number;
|
||||
/** Optional Agent Window outer height in CSS pixels (100..=7680). */
|
||||
height?: number;
|
||||
/** Defaults to true so existing clients preserve visible Agent Windows. */
|
||||
focused?: boolean;
|
||||
}
|
||||
|
||||
export interface SessionStartResult {
|
||||
@@ -99,7 +101,10 @@ export async function handleSessionStart(
|
||||
const sizeOrErr = validateWindowSize(params.width, params.height);
|
||||
if (isRpcError(sizeOrErr)) return sizeOrErr;
|
||||
try {
|
||||
const ctx = await manager.start(params.session_id, sizeOrErr);
|
||||
const ctx = await manager.start(params.session_id, {
|
||||
size: sizeOrErr,
|
||||
focused: params.focused,
|
||||
});
|
||||
return { agent_window_id: ctx.agentWindowId };
|
||||
} catch (err) {
|
||||
// chrome.windows.create / SessionManager failures are not CDP
|
||||
|
||||
@@ -42,7 +42,7 @@ Every automation task **must** follow this lifecycle. Do **not** rely on idle ti
|
||||
3. bsk session stop <id> → REQUIRED when done (even on error paths)
|
||||
```
|
||||
|
||||
Optional: `bsk session start --browser <instance-id-or-label>` when multiple browsers are connected (`bsk browsers` / error output lists them).
|
||||
Optional: `bsk session start --browser <instance-id-or-label>` when multiple browsers are connected (`bsk browsers` / error output lists them). Add `--no-focus` to open the Agent Window in the background without stealing focus from the user's current window.
|
||||
|
||||
Emergency cleanup: `bsk session stop --all` or the Agent Window overlay **Stop all**.
|
||||
|
||||
@@ -129,6 +129,7 @@ Details and flags: **`bsk <cmd> --help`**
|
||||
| Command | Summary |
|
||||
|---------|---------|
|
||||
| `bsk session start` | Open Agent Window (`--width`/`--height` for initial size); prints **4-letter session id** |
|
||||
| `bsk session start --no-focus` | Open Agent Window in the background without stealing focus |
|
||||
| `bsk session stop <id>` | End session, close Agent Window, auto-return borrowed tabs |
|
||||
| `bsk session stop --all` | Stop every active session |
|
||||
| `bsk session list` | List active sessions |
|
||||
|
||||
@@ -17,7 +17,7 @@ use crate::cli::business_rpc;
|
||||
use crate::cli::ensure_daemon::ensure_daemon;
|
||||
use crate::cli::error::{CliError, Format};
|
||||
use crate::cli::record_state;
|
||||
use crate::cli::session::{start_session, stop_session};
|
||||
use crate::cli::session::{SessionStartOptions, start_session, stop_session};
|
||||
|
||||
/// Max wait for the user to click 结束 in the browser (24 hours).
|
||||
const RECORD_AWAIT_TIMEOUT_MS: u32 = 86_400_000;
|
||||
@@ -85,7 +85,13 @@ fn dispatch_start(args: RecordStartArgs, format: Format) -> Result<(), CliError>
|
||||
}
|
||||
|
||||
let info = ensure_daemon().context("ensure daemon is running")?;
|
||||
let session = start_session(info.sock_path.clone(), args.browser, None, None)?;
|
||||
let session = start_session(
|
||||
info.sock_path.clone(),
|
||||
SessionStartOptions {
|
||||
browser: args.browser,
|
||||
..SessionStartOptions::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
let start_params = RecordStartParams {
|
||||
session_id: session.session_id.clone(),
|
||||
|
||||
@@ -64,6 +64,10 @@ pub struct SessionStartArgs {
|
||||
/// `--width` and `--height` must be given to take effect.
|
||||
#[arg(long, value_parser = window_size)]
|
||||
pub height: Option<u32>,
|
||||
|
||||
/// Open the Agent Window in the background without stealing focus.
|
||||
#[arg(long)]
|
||||
pub no_focus: bool,
|
||||
}
|
||||
|
||||
/// Parse a `--width` / `--height` Agent Window dimension (CSS pixels).
|
||||
@@ -99,6 +103,8 @@ struct StartParams {
|
||||
width: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
height: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
focused: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -173,7 +179,15 @@ fn run_start(sock: PathBuf, args: SessionStartArgs, format: Format) -> Result<()
|
||||
}
|
||||
});
|
||||
}
|
||||
let result = start_session(sock, args.browser, args.width, args.height);
|
||||
let result = start_session(
|
||||
sock,
|
||||
SessionStartOptions {
|
||||
browser: args.browser,
|
||||
width: args.width,
|
||||
height: args.height,
|
||||
focused: args.no_focus.then_some(false),
|
||||
},
|
||||
);
|
||||
waited.store(true, Ordering::SeqCst);
|
||||
match result {
|
||||
Ok(reply) => match format {
|
||||
@@ -197,20 +211,27 @@ fn run_start(sock: PathBuf, args: SessionStartArgs, format: Format) -> Result<()
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Options for [`start_session`]: browser selection plus Agent Window
|
||||
/// creation hints. `None` fields keep the extension-side defaults
|
||||
/// (focused window, browser-chosen size).
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct SessionStartOptions {
|
||||
pub browser: Option<String>,
|
||||
pub width: Option<u32>,
|
||||
pub height: Option<u32>,
|
||||
pub focused: Option<bool>,
|
||||
}
|
||||
|
||||
/// Start a session and open the Agent Window. Used by `session start` and `record start`.
|
||||
pub fn start_session(
|
||||
sock: PathBuf,
|
||||
browser: Option<String>,
|
||||
width: Option<u32>,
|
||||
height: Option<u32>,
|
||||
) -> Result<StartReply, CliError> {
|
||||
pub fn start_session(sock: PathBuf, opts: SessionStartOptions) -> Result<StartReply, CliError> {
|
||||
call(
|
||||
sock,
|
||||
Method::SessionStart,
|
||||
Some(StartParams {
|
||||
browser_instance_id: browser,
|
||||
width,
|
||||
height,
|
||||
browser_instance_id: opts.browser,
|
||||
width: opts.width,
|
||||
height: opts.height,
|
||||
focused: opts.focused,
|
||||
}),
|
||||
SESSION_START_IPC_TIMEOUT,
|
||||
)
|
||||
|
||||
@@ -42,8 +42,8 @@ use tracing::{debug, warn};
|
||||
use super::abort::AbortRegistry;
|
||||
use super::queue::{DEFAULT_TOOL_TIMEOUT, DispatchError};
|
||||
use super::sessions::{
|
||||
SessionId, StartSessionError, StopSessionError, snapshot_status_entries, start_session,
|
||||
stop_session,
|
||||
AgentWindowOptions, SessionId, StartSessionError, StopSessionError, snapshot_status_entries,
|
||||
start_session, stop_session,
|
||||
};
|
||||
use super::state::{DAEMON_VERSION, DaemonState, PROTOCOL_VERSION};
|
||||
|
||||
@@ -545,6 +545,8 @@ struct CliSessionStartParams {
|
||||
pub width: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub height: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub focused: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -638,6 +640,7 @@ async fn handle_session_start(state: &Arc<DaemonState>, params: Value) -> Result
|
||||
browser_instance_id: None,
|
||||
width: None,
|
||||
height: None,
|
||||
focused: None,
|
||||
}
|
||||
} else {
|
||||
serde_json::from_value(params).map_err(|err| RpcError {
|
||||
@@ -664,7 +667,10 @@ async fn handle_session_start(state: &Arc<DaemonState>, params: Value) -> Result
|
||||
&state.sessions,
|
||||
&state.tool_queues,
|
||||
params.browser_instance_id.as_deref(),
|
||||
window_size,
|
||||
AgentWindowOptions {
|
||||
size: window_size,
|
||||
focused: params.focused,
|
||||
},
|
||||
state.config.extension_connect_wait,
|
||||
DEFAULT_RPC_TIMEOUT,
|
||||
)
|
||||
|
||||
@@ -390,6 +390,17 @@ pub enum StopSessionError {
|
||||
/// of live sessions.
|
||||
const SESSION_ID_MAX_RESERVE_ATTEMPTS: u32 = 64;
|
||||
|
||||
/// Agent Window creation hints forwarded to the extension on
|
||||
/// `tool.session_start`. `None` fields keep the extension-side defaults
|
||||
/// (focused window, browser-chosen size).
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct AgentWindowOptions {
|
||||
/// Optional outer size as `(width, height)` CSS pixels.
|
||||
pub size: Option<(u32, u32)>,
|
||||
/// Optional focus hint (`None` = extension default: focused).
|
||||
pub focused: Option<bool>,
|
||||
}
|
||||
|
||||
/// Ask the chosen browser to create a fresh Agent Window for a brand-new
|
||||
/// session id, registering the result on success.
|
||||
///
|
||||
@@ -401,8 +412,7 @@ pub async fn start_session(
|
||||
sessions: &Arc<SessionRegistry>,
|
||||
queues: &Arc<ToolQueueRegistry>,
|
||||
requested: Option<&str>,
|
||||
// Optional Agent Window outer size as `(width, height)` CSS pixels.
|
||||
window_size: Option<(u32, u32)>,
|
||||
window: AgentWindowOptions,
|
||||
connect_wait: Duration,
|
||||
timeout_dur: Duration,
|
||||
) -> Result<Session, StartSessionError> {
|
||||
@@ -429,8 +439,9 @@ pub async fn start_session(
|
||||
let params = SessionStartParams {
|
||||
session_id: session_id.0.clone(),
|
||||
browser_instance_id: Some(client.id.0.clone()),
|
||||
width: window_size.map(|(width, _)| width),
|
||||
height: window_size.map(|(_, height)| height),
|
||||
width: window.size.map(|(width, _)| width),
|
||||
height: window.size.map(|(_, height)| height),
|
||||
focused: window.focused,
|
||||
};
|
||||
let rpc_id = next_rpc_id("sess-start");
|
||||
let request = RequestFrame {
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::time::Duration;
|
||||
use bsk::cli::daemon::{DaemonCmd, parse_duration};
|
||||
use bsk::cli::navigate::NavigateCmd;
|
||||
use bsk::cli::record::{RecordCmd, RecordSub};
|
||||
use bsk::cli::session::{SessionCmd, SessionSub};
|
||||
use bsk::{Cli, Command};
|
||||
use clap::Parser;
|
||||
|
||||
@@ -500,3 +501,15 @@ fn rejects_invalid_emulate_values() {
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_session_start_no_focus() {
|
||||
let cli = parse(&["bsk", "session", "start", "--no-focus"]);
|
||||
let Command::Session(SessionCmd {
|
||||
sub: SessionSub::Start(args),
|
||||
}) = cli.command
|
||||
else {
|
||||
panic!("expected session start subcommand");
|
||||
};
|
||||
assert!(args.no_focus);
|
||||
}
|
||||
|
||||
@@ -144,8 +144,9 @@ async fn session_start_stop_round_trip_via_ipc() {
|
||||
if let Frame::Request(req) = frame {
|
||||
let reply = match req.method {
|
||||
Method::ToolSessionStart => {
|
||||
let _: SessionStartParams =
|
||||
let params: SessionStartParams =
|
||||
serde_json::from_value(req.params.clone().unwrap()).unwrap();
|
||||
assert_eq!(params.focused, Some(false));
|
||||
let result = SessionStartResult {
|
||||
agent_window_id: Some(4242),
|
||||
};
|
||||
@@ -177,6 +178,7 @@ async fn session_start_stop_round_trip_via_ipc() {
|
||||
#[derive(serde::Serialize)]
|
||||
struct StartParams {
|
||||
browser_instance_id: Option<String>,
|
||||
focused: Option<bool>,
|
||||
}
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
struct StartReply {
|
||||
@@ -191,6 +193,7 @@ async fn session_start_stop_round_trip_via_ipc() {
|
||||
Method::SessionStart,
|
||||
Some(StartParams {
|
||||
browser_instance_id: None,
|
||||
focused: Some(false),
|
||||
}),
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
|
||||
@@ -12,6 +12,13 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"focused": {
|
||||
"description": "Whether the new Agent Window should take focus. Omitted means the extension's default (`true`) for compatibility with older clients.",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"height": {
|
||||
"description": "Optional Agent Window outer height in CSS pixels (100..=7680).",
|
||||
"type": [
|
||||
|
||||
@@ -16,6 +16,10 @@ pub struct SessionStartParams {
|
||||
/// Optional Agent Window outer height in CSS pixels (100..=7680).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub height: Option<u32>,
|
||||
/// Whether the new Agent Window should take focus. Omitted means the
|
||||
/// extension's default (`true`) for compatibility with older clients.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub focused: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -49,6 +53,23 @@ mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn session_start_focus_is_optional_and_round_trips_false() {
|
||||
let defaulted: SessionStartParams = serde_json::from_value(json!({
|
||||
"session_id": "aa11"
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(defaulted.focused, None);
|
||||
|
||||
let background: SessionStartParams = serde_json::from_value(json!({
|
||||
"session_id": "aa11",
|
||||
"focused": false
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(background.focused, Some(false));
|
||||
assert_eq!(serde_json::to_value(background).unwrap()["focused"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_stop_result_round_trips_auto_return_payload() {
|
||||
let result: SessionStopResult = serde_json::from_value(json!({
|
||||
|
||||
+2
-1
@@ -42,7 +42,7 @@ Every automation task **must** follow this lifecycle. Do **not** rely on idle ti
|
||||
3. bsk session stop <id> → REQUIRED when done (even on error paths)
|
||||
```
|
||||
|
||||
Optional: `bsk session start --browser <instance-id-or-label>` when multiple browsers are connected (`bsk browsers` / error output lists them).
|
||||
Optional: `bsk session start --browser <instance-id-or-label>` when multiple browsers are connected (`bsk browsers` / error output lists them). Add `--no-focus` to open the Agent Window in the background without stealing focus from the user's current window.
|
||||
|
||||
Emergency cleanup: `bsk session stop --all` or the Agent Window overlay **Stop all**.
|
||||
|
||||
@@ -129,6 +129,7 @@ Details and flags: **`bsk <cmd> --help`**
|
||||
| Command | Summary |
|
||||
|---------|---------|
|
||||
| `bsk session start` | Open Agent Window (`--width`/`--height` for initial size); prints **4-letter session id** |
|
||||
| `bsk session start --no-focus` | Open Agent Window in the background without stealing focus |
|
||||
| `bsk session stop <id>` | End session, close Agent Window, auto-return borrowed tabs |
|
||||
| `bsk session stop --all` | Stop every active session |
|
||||
| `bsk session list` | List active sessions |
|
||||
|
||||
Reference in New Issue
Block a user