Skip to content

Commit ec0aefe

Browse files
committed
fix(plugin): advertise codex experimental api
Send `capabilities.experimentalApi = true` during Codex app-server initialization so `thread/start` accepts `runtimeWorkspaceRoots` for the workspace-scoped chat session. Extend the live app-server smoke to start a thread with `runtimeWorkspaceRoots` without starting a model turn, covering the protocol path that the chat pane uses on submit.
1 parent dca8a6e commit ec0aefe

3 files changed

Lines changed: 56 additions & 38 deletions

File tree

docs/codex-integration-plan.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,8 @@ Acceptance checks:
168168
- Terminal smoke coverage can open the Codex Chat Window, submit a prompt to a
169169
fake app-server, and verify streamed assistant text is rendered.
170170
- Opt-in live app-server smoke coverage can initialize the installed Codex CLI
171-
over stdio and list project sessions without starting a model turn.
171+
over stdio, list project sessions, and start a thread with
172+
`runtimeWorkspaceRoots` without starting a model turn.
172173

173174
## Deferred Work
174175

scripts/smoke-test-codex-live-app-server.sh

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ try:
7979
"title": "Red Live Smoke",
8080
"version": "0",
8181
},
82+
"capabilities": {
83+
"experimentalApi": True,
84+
},
8285
},
8386
})
8487
initialize_result = read_response(0)
@@ -101,6 +104,21 @@ try:
101104
fail("codex app-server thread/list response was not an object")
102105
if "data" in list_result and not isinstance(list_result["data"], list):
103106
fail("codex app-server thread/list `data` field was not a list")
107+
send({
108+
"method": "thread/start",
109+
"id": 2,
110+
"params": {
111+
"cwd": cwd,
112+
"runtimeWorkspaceRoots": [cwd],
113+
},
114+
})
115+
start_result = read_response(2)
116+
thread = start_result.get("thread") if isinstance(start_result, dict) else None
117+
if not isinstance(thread, dict) or not isinstance(thread.get("id"), str):
118+
fail("codex app-server thread/start response did not include `thread.id`")
119+
roots = start_result.get("runtimeWorkspaceRoots") if isinstance(start_result, dict) else None
120+
if roots is not None and roots != [cwd]:
121+
fail("codex app-server thread/start returned unexpected `runtimeWorkspaceRoots`")
104122
print("Codex live app-server smoke passed.")
105123
finally:
106124
process.kill()

src/plugin/runtime.rs

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -641,17 +641,7 @@ async fn op_codex_app_server_request(
641641
let endpoint = take_codex_app_server_endpoint(&mut params);
642642
let mut client = open_codex_app_server_client(endpoint.as_deref()).await?;
643643

644-
let initialize = json!({
645-
"method": "initialize",
646-
"id": 0,
647-
"params": {
648-
"clientInfo": {
649-
"name": "red_codex_plugin",
650-
"title": "Red Codex Plugin",
651-
"version": env!("CARGO_PKG_VERSION")
652-
}
653-
}
654-
});
644+
let initialize = codex_initialize_request();
655645
client.send(initialize).await?;
656646

657647
timeout(
@@ -703,19 +693,7 @@ async fn run_codex_turn_inner(
703693

704694
let mut client = open_codex_app_server_client(endpoint.as_deref()).await?;
705695

706-
client
707-
.send(json!({
708-
"method": "initialize",
709-
"id": 0,
710-
"params": {
711-
"clientInfo": {
712-
"name": "red_codex_plugin",
713-
"title": "Red Codex Plugin",
714-
"version": env!("CARGO_PKG_VERSION")
715-
}
716-
}
717-
}))
718-
.await?;
696+
client.send(codex_initialize_request()).await?;
719697
timeout(
720698
Duration::from_secs(30),
721699
read_app_server_response(&mut client, 0),
@@ -941,6 +919,23 @@ fn is_app_server_request(value: &serde_json::Value) -> bool {
941919
&& value.get("error").is_none()
942920
}
943921

922+
fn codex_initialize_request() -> serde_json::Value {
923+
json!({
924+
"method": "initialize",
925+
"id": 0,
926+
"params": {
927+
"clientInfo": {
928+
"name": "red_codex_plugin",
929+
"title": "Red Codex Plugin",
930+
"version": env!("CARGO_PKG_VERSION")
931+
},
932+
"capabilities": {
933+
"experimentalApi": true
934+
}
935+
}
936+
})
937+
}
938+
944939
fn is_interactive_app_server_request_method(method: &str) -> bool {
945940
matches!(
946941
method,
@@ -1537,6 +1532,20 @@ mod tests {
15371532
use super::*;
15381533
use tokio::net::TcpListener;
15391534

1535+
fn assert_codex_initialize_request(initialize: &Value) {
1536+
assert_eq!(
1537+
initialize.get("method").and_then(Value::as_str),
1538+
Some("initialize")
1539+
);
1540+
assert_eq!(initialize.get("id").and_then(Value::as_i64), Some(0));
1541+
assert_eq!(
1542+
initialize
1543+
.pointer("/params/capabilities/experimentalApi")
1544+
.and_then(Value::as_bool),
1545+
Some(true)
1546+
);
1547+
}
1548+
15401549
#[tokio::test]
15411550
async fn test_runtime_plugin() {
15421551
let mut runtime = Runtime::new();
@@ -1672,11 +1681,7 @@ mod tests {
16721681
let mut stream = tokio_tungstenite::accept_async(socket).await.unwrap();
16731682

16741683
let initialize = read_fake_app_server_message(&mut stream).await;
1675-
assert_eq!(
1676-
initialize.get("method").and_then(Value::as_str),
1677-
Some("initialize")
1678-
);
1679-
assert_eq!(initialize.get("id").and_then(Value::as_i64), Some(0));
1684+
assert_codex_initialize_request(&initialize);
16801685
write_fake_app_server_message(
16811686
&mut stream,
16821687
json!({
@@ -1844,10 +1849,7 @@ mod tests {
18441849
let mut stream = tokio_tungstenite::accept_async(socket).await.unwrap();
18451850

18461851
let initialize = read_fake_app_server_message(&mut stream).await;
1847-
assert_eq!(
1848-
initialize.get("method").and_then(Value::as_str),
1849-
Some("initialize")
1850-
);
1852+
assert_codex_initialize_request(&initialize);
18511853
write_fake_app_server_message(&mut stream, json!({ "id": 0, "result": {} })).await;
18521854

18531855
let initialized = read_fake_app_server_message(&mut stream).await;
@@ -1981,10 +1983,7 @@ mod tests {
19811983
let mut stream = tokio_tungstenite::accept_async(socket).await.unwrap();
19821984

19831985
let initialize = read_fake_app_server_message(&mut stream).await;
1984-
assert_eq!(
1985-
initialize.get("method").and_then(Value::as_str),
1986-
Some("initialize")
1987-
);
1986+
assert_codex_initialize_request(&initialize);
19881987
write_fake_app_server_message(&mut stream, json!({ "id": 0, "result": {} })).await;
19891988

19901989
let initialized = read_fake_app_server_message(&mut stream).await;

0 commit comments

Comments
 (0)