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
13 changes: 13 additions & 0 deletions packages/sdk/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,19 @@ export class AgentRelayClient {
return this.requestOk<{ name: string; bytes_written: number }>('send_input', { name, data });
}

async resizePty(
name: string,
rows: number,
cols: number
): Promise<{ name: string; rows: number; cols: number }> {
await this.start();
return this.requestOk<{ name: string; rows: number; cols: number }>('resize_pty', {
name,
rows,
cols,
});
}

async setModel(
name: string,
model: string,
Expand Down
9 changes: 9 additions & 0 deletions packages/sdk/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ export type SdkToBroker =
* the cache instead of waiting on individual HTTP registrations. */
type: 'preflight_agents';
payload: { agents: Array<{ name: string; cli: string }> };
}
| {
/** Resize a PTY agent's terminal dimensions. */
type: 'resize_pty';
payload: { name: string; rows: number; cols: number };
};

export interface PendingDeliveryInfo {
Expand Down Expand Up @@ -365,6 +370,10 @@ export type BrokerToWorker =
| {
type: 'ping';
payload: { ts_ms: number };
}
| {
type: 'resize_pty';
payload: { rows: number; cols: number };
};

export type WorkerToBroker =
Expand Down
77 changes: 77 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,13 @@ struct SendInputPayload {
data: String,
}

#[derive(Debug, Deserialize)]
struct ResizePtyPayload {
name: String,
rows: u16,
cols: u16,
}

#[derive(Debug, Deserialize)]
struct SetModelPayload {
name: String,
Expand Down Expand Up @@ -4436,6 +4443,76 @@ async fn handle_sdk_frame(
.await?;
Ok(false)
}
"resize_pty" => {
let payload: ResizePtyPayload = serde_json::from_value(frame.payload)
.context("resize_pty payload must contain `name`, `rows`, and `cols`")?;

Comment thread
willwashburn marked this conversation as resolved.
if payload.rows == 0 || payload.cols == 0 {
send_error(
out_tx,
frame.request_id,
"invalid_dimensions",
"rows and cols must be >= 1".to_string(),
false,
None,
)
.await?;
return Ok(false);
}

let Some(handle) = workers.workers.get(&payload.name) else {
send_error(
out_tx,
frame.request_id,
"agent_not_found",
format!("unknown worker '{}'", payload.name),
false,
None,
)
.await?;
return Ok(false);
};

if handle.spec.runtime != AgentRuntime::Pty {
send_error(
out_tx,
frame.request_id,
"unsupported_operation",
format!(
"resize_pty is only supported for PTY agents, '{}' is {:?}",
payload.name, handle.spec.runtime
),
false,
None,
)
.await?;
return Ok(false);
}

workers
.send_to_worker(
&payload.name,
"resize_pty",
None,
json!({
"rows": payload.rows,
"cols": payload.cols,
}),
)
.await?;

send_ok(
out_tx,
frame.request_id,
json!({
"name": payload.name,
"rows": payload.rows,
"cols": payload.cols,
}),
)
.await?;
Ok(false)
}
"set_model" => {
let payload: SetModelPayload = serde_json::from_value(frame.payload)
.context("set_model payload must contain `name` and `model`")?;
Expand Down
21 changes: 21 additions & 0 deletions src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,10 @@ pub enum BrokerToWorker {
Ping {
ts_ms: u64,
},
ResizePty {
rows: u16,
cols: u16,
},
Comment thread
willwashburn marked this conversation as resolved.
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
Expand Down Expand Up @@ -446,4 +450,21 @@ mod tests {
let decoded: AgentSpec = serde_json::from_str(&encoded).unwrap();
assert_eq!(decoded.provider, Some(HeadlessProvider::Opencode));
}

#[test]
fn broker_to_worker_resize_pty_round_trip() {
let msg = BrokerToWorker::ResizePty {
rows: 40,
cols: 120,
};
let encoded = serde_json::to_string(&msg).unwrap();
let decoded: BrokerToWorker = serde_json::from_str(&encoded).unwrap();
assert_eq!(decoded, msg);

// Verify wire format uses snake_case tag
let raw: Value = serde_json::from_str(&encoded).unwrap();
assert_eq!(raw["type"], "resize_pty");
assert_eq!(raw["payload"]["rows"], 40);
assert_eq!(raw["payload"]["cols"], 120);
}
}
29 changes: 29 additions & 0 deletions src/pty_worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,35 @@ pub(crate) async fn run_pty_worker(cmd: PtyCommand) -> Result<()> {
"shutdown_worker" => {
running = false;
}
"resize_pty" => {
#[derive(serde::Deserialize)]
struct ResizePtyPayload { rows: u16, cols: u16 }
match serde_json::from_value::<ResizePtyPayload>(frame.payload) {
Ok(p) if p.rows > 0 && p.cols > 0 => {
if let Err(e) = pty.resize(p.rows, p.cols) {
tracing::warn!(
target = "agent_relay::worker::pty",
rows = p.rows, cols = p.cols, error = %e,
"failed to resize pty"
);
}
}
Ok(_) => {
let _ = send_frame(&out_tx, "worker_error", frame.request_id, json!({
"code": "invalid_dimensions",
"message": "rows and cols must be >= 1",
"retryable": false
})).await;
}
Err(e) => {
let _ = send_frame(&out_tx, "worker_error", frame.request_id, json!({
"code": "invalid_payload",
"message": e.to_string(),
"retryable": false
})).await;
}
}
}
"ping" => {
let ts = frame.payload.get("ts_ms").and_then(Value::as_u64).unwrap_or_default();
let _ = send_frame(&out_tx, "pong", frame.request_id, json!({"ts_ms": ts})).await;
Expand Down
Loading