Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/agent-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,13 @@ export interface SpawnAgentParams {
auto_archive_on_done?: boolean;
max_cost_per_agent?: number;
crash_recover?: boolean;
/** Internal lifecycle hook: runs immediately after cmux creates the surface,
* before launcher I/O or readiness polling can give the user time to move.
*/
on_surface_created?: (surface: {
surface: string;
workspace?: string;
}) => void | Promise<void>;
}

export interface SpawnAgentResult {
Expand Down Expand Up @@ -4591,6 +4598,14 @@ export class AgentEngine {
await this.cleanupUnboundCreatedSurface(surface, "agent-placement");
throw error;
}
try {
await spawnParams.on_surface_created?.({
surface: surface.surface,
workspace: surface.actual_workspace ?? surface.workspace,
});
} catch {
// Focus observation is advisory and must never discard a created handle.
}

// 2. Write initial state (creating → booting)
const now = new Date().toISOString();
Expand Down
28 changes: 22 additions & 6 deletions src/cmux-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,10 +378,6 @@ export class CmuxClient {
focus?: boolean;
},
): Promise<CmuxNewSplitResult> {
if (opts?.focus === false) {
throw new Error("cmux does not support creating unfocused splits");
}

if (opts?.type === "browser") {
if (opts.surface || opts.pane) {
throw new Error(
Expand All @@ -392,6 +388,8 @@ export class CmuxClient {
const args = ["new-pane", "--type", "browser", "--direction", direction];
if (opts.workspace) args.push("--workspace", opts.workspace);
if (opts.url) args.push("--url", opts.url);
if (opts.focus !== undefined)
args.push("--focus", String(opts.focus));

const raw = await this.run(args);
const parsed = this.parse<Record<string, unknown>>(raw, "new-pane");
Expand All @@ -404,6 +402,8 @@ export class CmuxClient {

const args = ["new-split", direction];
if (opts?.workspace) args.push("--workspace", opts.workspace);
if (opts?.focus !== undefined)
args.push("--focus", String(opts.focus));
const anchorSurface =
opts?.surface ??
(opts?.pane
Expand Down Expand Up @@ -535,6 +535,20 @@ export class CmuxClient {
await this.run(args);
}

async focusSurface(
surface: string,
opts?: { workspace?: string },
): Promise<void> {
await this.run([
"rpc",
"surface.focus",
JSON.stringify({
surface_id: surface,
...(opts?.workspace ? { workspace_id: opts.workspace } : {}),
}),
]);
}

async createWorkspace(
title: string,
): Promise<{ workspace: string; title: string }> {
Expand Down Expand Up @@ -715,8 +729,10 @@ export class CmuxClient {
return this.parseStatusOutput(raw);
}

async identify(surface: string): Promise<CmuxIdentifyResult> {
const raw = await this.run(["identify", "--surface", surface]);
async identify(surface?: string): Promise<CmuxIdentifyResult> {
const args = ["identify"];
if (surface) args.push("--surface", surface);
const raw = await this.run(args);
return this.parse(raw, "identify");
}

Expand Down
39 changes: 33 additions & 6 deletions src/cmux-socket-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,23 @@ export class CmuxSocketClient {
}
}

async focusSurface(
surface: string,
opts?: { workspace?: string },
): Promise<void> {
try {
await this.call("surface.focus", {
surface_id: surface,
...(opts?.workspace ? { workspace_id: opts.workspace } : {}),
});
} catch (e) {
if (this.isMethodNotFound(e) && this.cliFallback) {
return this.cliFallbackPinned()!.focusSurface(surface, opts);
}
throw e;
}
}

async createWorkspace(
title: string,
): Promise<{ workspace: string; title: string }> {
Expand Down Expand Up @@ -392,10 +409,18 @@ export class CmuxSocketClient {
focus?: boolean;
},
): Promise<CmuxNewSplitResult> {
if (opts?.focus === false) {
throw new CmuxSocketError(
"cmux does not support creating unfocused splits",
);
// The installed CLI documents --focus, but the v2 surface.split focus
// parameter is not part of cmuxlayer's verified socket contract. Route an
// explicit preference through the pinned CLI instead of risking a silently
// ignored socket field.
if (opts?.focus !== undefined) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High src/cmux-socket-client.ts:416

newSplit in CmuxSocketClient throws unsupported_focus_option when focus: true is passed on a socket-only instance without a CLI fallback, even though true is the socket's default behavior. Only focus: false needs to be routed through the CLI. The current guard at line 416 checks opts?.focus !== undefined, which catches true and causes a regression for callers that pass focus: true explicitly.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/cmux-socket-client.ts around line 416:

`newSplit` in `CmuxSocketClient` throws `unsupported_focus_option` when `focus: true` is passed on a socket-only instance without a CLI fallback, even though `true` is the socket's default behavior. Only `focus: false` needs to be routed through the CLI. The current guard at line 416 checks `opts?.focus !== undefined`, which catches `true` and causes a regression for callers that pass `focus: true` explicitly.

if (!this.cliFallback) {
throw new CmuxSocketError(
"newSplit focus requires the CLI fallback",
"unsupported_focus_option",
);
}
return this.cliFallbackPinned()!.newSplit(direction, opts);
}

if (opts?.type === "browser") {
Expand Down Expand Up @@ -742,7 +767,7 @@ export class CmuxSocketClient {
}
}

async identify(surface: string): Promise<{
async identify(surface?: string): Promise<{
caller?: {
workspace_ref?: string;
surface_ref?: string;
Expand All @@ -754,7 +779,9 @@ export class CmuxSocketClient {
pane_ref?: string;
};
}> {
return this.call("system.identify", { surface_id: surface });
return this.call("system.identify", {
...(surface ? { surface_id: surface } : {}),
});
}

async browser(args: string[]): Promise<unknown> {
Expand Down
1 change: 1 addition & 0 deletions src/cmux-transport-self-heal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ const FORWARDED_ASYNC_METHODS = [
"pasteText",
"sendKey",
"selectWorkspace",
"focusSurface",
"createWorkspace",
"deleteWorkspace",
"readScreen",
Expand Down
Loading
Loading