feat: support Agent Window sizing (session start --width/--height, window resize) (#52)

- `bsk session start --width <px> --height <px>` opens the Agent Window
  with the given outer size (chrome.windows.create)
- new `bsk window resize --session <id> --width <px> --height <px>`
  resizes the Agent Window at runtime (chrome.windows.update)
- both validated on CLI, daemon and extension sides (100..=7680 CSS px)
- protocol: new tool.window_resize method (BrowserMutation) and
  optional width/height on tool.session_start params; schemas regenerated
This commit is contained in:
BB-fat
2026-08-05 11:19:10 +08:00
committed by GitHub
parent d7f0f1e78c
commit 49378d70de
25 changed files with 725 additions and 10 deletions
+5
View File
@@ -30,6 +30,7 @@ pub mod status;
pub mod tab;
pub mod update;
pub mod waits;
pub mod window;
use clap::{Args, Parser, Subcommand};
@@ -50,6 +51,7 @@ use crate::cli::snapshot::SnapshotArgs;
use crate::cli::tab::TabCmd;
use crate::cli::update::UpdateArgs;
use crate::cli::waits::{WaitForNavigationArgs, WaitMsArgs};
use crate::cli::window::WindowCmd;
/// Tool calls wait slightly longer than the daemon's 30s tool timeout so
/// callers receive the structured daemon timeout instead of dropping the
@@ -118,6 +120,9 @@ pub enum Command {
/// Tab management commands.
Tab(TabCmd),
/// Agent Window management commands.
Window(WindowCmd),
/// Capture a PNG of the active tab or a snapshot ref element.
Screenshot(ScreenshotArgs),
+1 -1
View File
@@ -85,7 +85,7 @@ 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)?;
let session = start_session(info.sock_path.clone(), args.browser, None, None)?;
let start_params = RecordStartParams {
session_id: session.session_id.clone(),
+42 -2
View File
@@ -54,6 +54,30 @@ pub struct SessionStartArgs {
/// are connected).
#[arg(long)]
pub browser: Option<String>,
/// Agent Window outer width in CSS pixels (100..=7680). Both
/// `--width` and `--height` must be given to take effect.
#[arg(long, value_parser = window_size)]
pub width: Option<u32>,
/// Agent Window outer height in CSS pixels (100..=7680). Both
/// `--width` and `--height` must be given to take effect.
#[arg(long, value_parser = window_size)]
pub height: Option<u32>,
}
/// Parse a `--width` / `--height` Agent Window dimension (CSS pixels).
fn window_size(s: &str) -> Result<u32, String> {
let value: u32 = s
.parse()
.map_err(|_| format!("invalid window dimension {s:?}: expected a positive integer"))?;
if (100..=7680).contains(&value) {
Ok(value)
} else {
Err(format!(
"window dimension {value} out of range (100..=7680)"
))
}
}
#[derive(Debug, Clone, Args)]
@@ -71,6 +95,10 @@ pub struct SessionStopArgs {
struct StartParams {
#[serde(skip_serializing_if = "Option::is_none")]
browser_instance_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
width: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
height: Option<u32>,
}
#[derive(Debug, Deserialize)]
@@ -124,6 +152,11 @@ pub fn dispatch(cmd: SessionCmd, format: Format) -> Result<(), CliError> {
}
fn run_start(sock: PathBuf, args: SessionStartArgs, format: Format) -> Result<(), CliError> {
if args.width.is_some() != args.height.is_some() {
return Err(CliError::Local(anyhow::anyhow!(
"--width and --height must be given together"
)));
}
// The daemon may hold `session.start` open for up to
// `EXTENSION_CONNECT_WAIT` while a just-woken service worker
// reconnects. Let the user know we are waiting rather than hung —
@@ -140,7 +173,7 @@ fn run_start(sock: PathBuf, args: SessionStartArgs, format: Format) -> Result<()
}
});
}
let result = start_session(sock, args.browser);
let result = start_session(sock, args.browser, args.width, args.height);
waited.store(true, Ordering::SeqCst);
match result {
Ok(reply) => match format {
@@ -165,12 +198,19 @@ fn run_start(sock: PathBuf, args: SessionStartArgs, format: Format) -> Result<()
}
/// Start a session and open the Agent Window. Used by `session start` and `record start`.
pub fn start_session(sock: PathBuf, browser: Option<String>) -> Result<StartReply, CliError> {
pub fn start_session(
sock: PathBuf,
browser: Option<String>,
width: Option<u32>,
height: Option<u32>,
) -> Result<StartReply, CliError> {
call(
sock,
Method::SessionStart,
Some(StartParams {
browser_instance_id: browser,
width,
height,
}),
SESSION_START_IPC_TIMEOUT,
)
+103
View File
@@ -0,0 +1,103 @@
//! `bsk window …` subcommands — Agent Window management.
use std::path::PathBuf;
use anyhow::Context;
use bsk_protocol::Method;
use bsk_protocol::tools::{WindowResizeParams, WindowResizeResult};
use clap::{Args, Subcommand};
use crate::cli::TOOL_IPC_TIMEOUT;
use crate::cli::ensure_daemon::ensure_daemon;
use crate::cli::error::{CliError, Format};
#[derive(Debug, Clone, Args)]
pub struct WindowCmd {
#[command(subcommand)]
pub sub: WindowSub,
}
#[derive(Debug, Clone, Subcommand)]
pub enum WindowSub {
/// Resize the session's Agent Window.
Resize(WindowResizeArgs),
}
#[derive(Debug, Clone, Args)]
pub struct WindowResizeArgs {
/// Session id (must be active).
#[arg(long)]
pub session: String,
/// New Agent Window outer width in CSS pixels (100..=7680).
#[arg(long, value_parser = window_size)]
pub width: u32,
/// New Agent Window outer height in CSS pixels (100..=7680).
#[arg(long, value_parser = window_size)]
pub height: u32,
}
/// Parse a `--width` / `--height` Agent Window dimension (CSS pixels).
fn window_size(s: &str) -> Result<u32, String> {
let value: u32 = s
.parse()
.map_err(|_| format!("invalid window dimension {s:?}: expected a positive integer"))?;
if (100..=7680).contains(&value) {
Ok(value)
} else {
Err(format!(
"window dimension {value} out of range (100..=7680)"
))
}
}
pub fn dispatch(cmd: WindowCmd, format: Format) -> Result<(), CliError> {
let info = ensure_daemon().context("ensure daemon is running")?;
match cmd.sub {
WindowSub::Resize(args) => run_resize(info.sock_path, args, format),
}
}
fn run_resize(sock: PathBuf, args: WindowResizeArgs, format: Format) -> Result<(), CliError> {
let params = WindowResizeParams {
session_id: args.session,
width: args.width,
height: args.height,
};
let reply: WindowResizeResult =
ipc_call("window-resize-1", Method::ToolWindowResize, sock, params)?;
match format {
Format::Json => {
let json = serde_json::to_string_pretty(&reply)
.map_err(|e| CliError::Local(anyhow::anyhow!(e)))?;
println!("{json}");
}
Format::Human => {
println!(
"resized window_id={} to {}x{}",
reply.window_id, reply.width, reply.height
);
}
}
Ok(())
}
fn ipc_call<P, R>(
rpc_id_prefix: &'static str,
method: Method,
sock: PathBuf,
params: P,
) -> Result<R, CliError>
where
P: serde::Serialize + Send + 'static,
R: serde::de::DeserializeOwned + Send + 'static,
{
crate::cli::business_rpc::call::<P, R>(
sock,
rpc_id_prefix,
method,
Some(params),
TOOL_IPC_TIMEOUT,
)
}
+21
View File
@@ -229,6 +229,7 @@ pub fn full_handler(status: DaemonStatus, state: Arc<DaemonState>) -> RpcHandler
| Method::ToolTabSelect
| Method::ToolTabBorrow
| Method::ToolTabReturn
| Method::ToolWindowResize
| Method::ToolScreenshot
| Method::ToolConsole
| Method::ToolNetwork
@@ -538,6 +539,10 @@ fn tool_dispatch_timeout(params: &Value) -> Result<Duration, RpcError> {
struct CliSessionStartParams {
#[serde(default)]
pub browser_instance_id: Option<String>,
#[serde(default)]
pub width: Option<u32>,
#[serde(default)]
pub height: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -629,6 +634,8 @@ async fn handle_session_start(state: &Arc<DaemonState>, params: Value) -> Result
let params: CliSessionStartParams = if params.is_null() {
CliSessionStartParams {
browser_instance_id: None,
width: None,
height: None,
}
} else {
serde_json::from_value(params).map_err(|err| RpcError {
@@ -637,11 +644,25 @@ async fn handle_session_start(state: &Arc<DaemonState>, params: Value) -> Result
data: None,
})?
};
// `--width` without `--height` (or vice versa) is rejected: the
// extension only accepts a complete size pair.
let window_size = match (params.width, params.height) {
(Some(width), Some(height)) => Some((width, height)),
(None, None) => None,
_ => {
return Err(RpcError {
code: ErrorCode::InvalidParams,
message: "width and height must be given together".into(),
data: None,
});
}
};
match start_session(
&state.browsers,
&state.sessions,
&state.tool_queues,
params.browser_instance_id.as_deref(),
window_size,
state.config.extension_connect_wait,
DEFAULT_RPC_TIMEOUT,
)
+4
View File
@@ -401,6 +401,8 @@ 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)>,
connect_wait: Duration,
timeout_dur: Duration,
) -> Result<Session, StartSessionError> {
@@ -427,6 +429,8 @@ 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),
};
let rpc_id = next_rpc_id("sess-start");
let request = RequestFrame {
+1
View File
@@ -80,6 +80,7 @@ fn dispatch(cli: Cli, format: Format) -> Result<(), CliError> {
Command::Session(cmd) => cli::session::dispatch(cmd, format),
Command::Browsers => cli::browsers::dispatch(format),
Command::Tab(cmd) => cli::tab::dispatch(cmd, format),
Command::Window(cmd) => cli::window::dispatch(cmd, format),
Command::Screenshot(args) => cli::screenshot::dispatch(args, format),
Command::Snapshot(args) => cli::snapshot::dispatch(args, format),
Command::Observe(args) => cli::observe::dispatch(args, format),