Skip to content

Commit 127d582

Browse files
committed
feat(composer): add /fast command and service-tier forwarding
1 parent dc85b91 commit 127d582

38 files changed

Lines changed: 916 additions & 70 deletions

docs/app-server-events.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,9 @@ These are v2 request methods CodexMonitor currently sends to Codex app-server:
166166
- `skills/list`
167167
- `app/list`
168168

169+
Notes:
170+
- `turn/start` now forwards the optional `serviceTier` override (`"fast"` for `/fast`, `null` for default/off) alongside `model`, `effort`, and `collaborationMode`.
171+
169172
## Missing Client Requests (Codex v2 ClientRequest Methods)
170173

171174
Compared against Codex v2 request methods, CodexMonitor currently does not send:

src-tauri/src/bin/codex_monitor_daemon.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,7 @@ impl DaemonState {
802802
text: String,
803803
model: Option<String>,
804804
effort: Option<String>,
805+
service_tier: Option<Option<String>>,
805806
access_mode: Option<String>,
806807
images: Option<Vec<String>>,
807808
app_mentions: Option<Vec<Value>>,
@@ -815,6 +816,7 @@ impl DaemonState {
815816
text,
816817
model,
817818
effort,
819+
service_tier,
818820
access_mode,
819821
images,
820822
app_mentions,

src-tauri/src/bin/codex_monitor_daemon/rpc.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,21 @@ pub(super) fn parse_optional_string(value: &Value, key: &str) -> Option<String>
8585
}
8686
}
8787

88+
pub(super) fn parse_optional_nullable_string(
89+
value: &Value,
90+
key: &str,
91+
) -> Option<Option<String>> {
92+
match value {
93+
Value::Object(map) => match map.get(key) {
94+
Some(Value::Null) => Some(None),
95+
Some(Value::String(value)) => Some(Some(value.to_string())),
96+
Some(_) => None,
97+
None => None,
98+
},
99+
_ => None,
100+
}
101+
}
102+
88103
pub(super) fn parse_optional_u32(value: &Value, key: &str) -> Option<u32> {
89104
match value {
90105
Value::Object(map) => map.get(key).and_then(|value| value.as_u64()).and_then(|v| {

src-tauri/src/bin/codex_monitor_daemon/rpc/codex.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ pub(super) async fn try_handle(
160160
};
161161
let model = parse_optional_string(params, "model");
162162
let effort = parse_optional_string(params, "effort");
163+
let service_tier = parse_optional_nullable_string(params, "serviceTier");
163164
let access_mode = parse_optional_string(params, "accessMode");
164165
let images = parse_optional_string_array(params, "images");
165166
let app_mentions = parse_optional_value(params, "appMentions")
@@ -173,6 +174,7 @@ pub(super) async fn try_handle(
173174
text,
174175
model,
175176
effort,
177+
service_tier,
176178
access_mode,
177179
images,
178180
app_mentions,

src-tauri/src/codex/mod.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use crate::backend::events::AppServerEvent;
1414
use crate::event_sink::TauriEventSink;
1515
use crate::remote_backend;
1616
use crate::shared::agents_config_core;
17-
use crate::shared::codex_core;
17+
use crate::shared::codex_core::{self, insert_optional_nullable_string};
1818
use crate::state::AppState;
1919
use crate::types::WorkspaceEntry;
2020

@@ -322,6 +322,7 @@ pub(crate) async fn send_user_message(
322322
text: String,
323323
model: Option<String>,
324324
effort: Option<String>,
325+
service_tier: Option<Option<String>>,
325326
access_mode: Option<String>,
326327
images: Option<Vec<String>>,
327328
app_mentions: Option<Vec<Value>>,
@@ -342,6 +343,7 @@ pub(crate) async fn send_user_message(
342343
payload.insert("text".to_string(), json!(text));
343344
payload.insert("model".to_string(), json!(model));
344345
payload.insert("effort".to_string(), json!(effort));
346+
insert_optional_nullable_string(&mut payload, "serviceTier", service_tier);
345347
payload.insert("accessMode".to_string(), json!(access_mode));
346348
payload.insert("images".to_string(), json!(images));
347349
payload.insert("appMentions".to_string(), json!(app_mentions));
@@ -367,6 +369,7 @@ pub(crate) async fn send_user_message(
367369
text,
368370
model,
369371
effort,
372+
service_tier,
370373
access_mode,
371374
images,
372375
app_mentions,
@@ -485,7 +488,14 @@ pub(crate) async fn start_review(
485488
.await;
486489
}
487490

488-
codex_core::start_review_core(&state.sessions, workspace_id, thread_id, target, delivery).await
491+
codex_core::start_review_core(
492+
&state.sessions,
493+
workspace_id,
494+
thread_id,
495+
target,
496+
delivery,
497+
)
498+
.await
489499
}
490500

491501
#[tauri::command]

src-tauri/src/shared/codex_core.rs

Lines changed: 82 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -365,23 +365,34 @@ fn build_turn_input_items(
365365
input.push(json!({ "type": "mention", "name": name, "path": path }));
366366
}
367367
}
368-
if input.is_empty() {
369-
return Err("empty user message".to_string());
370-
}
371-
Ok(input)
372-
}
373-
374-
pub(crate) async fn send_user_message_core(
375-
sessions: &Mutex<HashMap<String, Arc<WorkspaceSession>>>,
376-
workspaces: &Mutex<HashMap<String, WorkspaceEntry>>,
377-
workspace_id: String,
378-
thread_id: String,
379-
text: String,
380-
model: Option<String>,
381-
effort: Option<String>,
382-
access_mode: Option<String>,
383-
images: Option<Vec<String>>,
384-
app_mentions: Option<Vec<Value>>,
368+
if input.is_empty() {
369+
return Err("empty user message".to_string());
370+
}
371+
Ok(input)
372+
}
373+
374+
pub(crate) fn insert_optional_nullable_string(
375+
params: &mut Map<String, Value>,
376+
key: &str,
377+
value: Option<Option<String>>,
378+
) {
379+
if let Some(value) = value {
380+
params.insert(key.to_string(), json!(value));
381+
}
382+
}
383+
384+
pub(crate) async fn send_user_message_core(
385+
sessions: &Mutex<HashMap<String, Arc<WorkspaceSession>>>,
386+
workspaces: &Mutex<HashMap<String, WorkspaceEntry>>,
387+
workspace_id: String,
388+
thread_id: String,
389+
text: String,
390+
model: Option<String>,
391+
effort: Option<String>,
392+
service_tier: Option<Option<String>>,
393+
access_mode: Option<String>,
394+
images: Option<Vec<String>>,
395+
app_mentions: Option<Vec<Value>>,
385396
collaboration_mode: Option<Value>,
386397
) -> Result<Value, String> {
387398
let session = get_session_clone(sessions, &workspace_id).await?;
@@ -408,14 +419,15 @@ pub(crate) async fn send_user_message_core(
408419
let mut params = Map::new();
409420
params.insert("threadId".to_string(), json!(thread_id));
410421
params.insert("input".to_string(), json!(input));
411-
params.insert("cwd".to_string(), json!(workspace_path));
412-
params.insert("approvalPolicy".to_string(), json!(approval_policy));
413-
params.insert("sandboxPolicy".to_string(), json!(sandbox_policy));
414-
params.insert("model".to_string(), json!(model));
415-
params.insert("effort".to_string(), json!(effort));
416-
if let Some(mode) = collaboration_mode {
417-
if !mode.is_null() {
418-
params.insert("collaborationMode".to_string(), mode);
422+
params.insert("cwd".to_string(), json!(workspace_path));
423+
params.insert("approvalPolicy".to_string(), json!(approval_policy));
424+
params.insert("sandboxPolicy".to_string(), json!(sandbox_policy));
425+
params.insert("model".to_string(), json!(model));
426+
params.insert("effort".to_string(), json!(effort));
427+
insert_optional_nullable_string(&mut params, "serviceTier", service_tier);
428+
if let Some(mode) = collaboration_mode {
429+
if !mode.is_null() {
430+
params.insert("collaborationMode".to_string(), mode);
419431
}
420432
}
421433
session
@@ -470,24 +482,24 @@ pub(crate) async fn turn_interrupt_core(
470482
.await
471483
}
472484

473-
pub(crate) async fn start_review_core(
474-
sessions: &Mutex<HashMap<String, Arc<WorkspaceSession>>>,
475-
workspace_id: String,
476-
thread_id: String,
477-
target: Value,
478-
delivery: Option<String>,
479-
) -> Result<Value, String> {
480-
let session = get_session_clone(sessions, &workspace_id).await?;
481-
let mut params = Map::new();
482-
params.insert("threadId".to_string(), json!(thread_id));
483-
params.insert("target".to_string(), target);
484-
if let Some(delivery) = delivery {
485-
params.insert("delivery".to_string(), json!(delivery));
486-
}
487-
session
488-
.send_request_for_workspace(&workspace_id, "review/start", Value::Object(params))
489-
.await
490-
}
485+
pub(crate) async fn start_review_core(
486+
sessions: &Mutex<HashMap<String, Arc<WorkspaceSession>>>,
487+
workspace_id: String,
488+
thread_id: String,
489+
target: Value,
490+
delivery: Option<String>,
491+
) -> Result<Value, String> {
492+
let session = get_session_clone(sessions, &workspace_id).await?;
493+
let mut params = Map::new();
494+
params.insert("threadId".to_string(), json!(thread_id));
495+
params.insert("target".to_string(), target);
496+
if let Some(delivery) = delivery {
497+
params.insert("delivery".to_string(), json!(delivery));
498+
}
499+
session
500+
.send_request_for_workspace(&workspace_id, "review/start", Value::Object(params))
501+
.await
502+
}
491503

492504
pub(crate) async fn model_list_core(
493505
sessions: &Mutex<HashMap<String, Arc<WorkspaceSession>>>,
@@ -786,9 +798,10 @@ pub(crate) async fn get_config_model_core(
786798
Ok(json!({ "model": model }))
787799
}
788800

789-
#[cfg(test)]
790-
mod tests {
791-
use super::*;
801+
#[cfg(test)]
802+
mod tests {
803+
use super::*;
804+
use serde_json::Value;
792805

793806
#[test]
794807
fn normalize_strips_file_uri_prefix() {
@@ -851,7 +864,7 @@ mod tests {
851864
}
852865

853866
#[test]
854-
fn read_image_data_url_core_succeeds_with_file_uri_for_real_file() {
867+
fn read_image_data_url_core_succeeds_with_file_uri_for_real_file() {
855868
let dir = std::env::temp_dir().join("codex_monitor_test");
856869
std::fs::create_dir_all(&dir).unwrap();
857870
let img_path = dir.join("test_photo.png");
@@ -902,7 +915,25 @@ mod tests {
902915
"plain filesystem paths with percent sequences should not be decoded, got: {:?}",
903916
result3.err()
904917
);
905-
906-
let _ = std::fs::remove_dir_all(&dir);
907-
}
908-
}
918+
919+
let _ = std::fs::remove_dir_all(&dir);
920+
}
921+
922+
#[test]
923+
fn insert_optional_nullable_string_omits_missing_and_preserves_null() {
924+
let mut params = Map::new();
925+
926+
insert_optional_nullable_string(&mut params, "serviceTier", None);
927+
assert!(!params.contains_key("serviceTier"));
928+
929+
insert_optional_nullable_string(&mut params, "serviceTier", Some(None));
930+
assert_eq!(params.get("serviceTier"), Some(&Value::Null));
931+
932+
insert_optional_nullable_string(
933+
&mut params,
934+
"serviceTier",
935+
Some(Some("fast".to_string())),
936+
);
937+
assert_eq!(params.get("serviceTier"), Some(&json!("fast")));
938+
}
939+
}

0 commit comments

Comments
 (0)