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:
BB-fat
2026-08-12 19:58:53 +08:00
committed by GitHub
co-authored by alectimison-maker
parent 0ce497f017
commit f443cfae74
17 changed files with 235 additions and 43 deletions
@@ -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 () => {
+6 -1
View File
@@ -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