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
6 changes: 5 additions & 1 deletion codex-rs/features/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1680,7 +1680,11 @@ pub const FEATURES: &[FeatureSpec] = &[
FeatureSpec {
id: Feature::RealtimeConversation,
key: "realtime_conversation",
stage: Stage::Removed,
stage: Stage::Experimental {
name: "Voice conversations",
menu_description: "Talk with Codex using /voice.",
announcement: "NEW: Voice conversations can now be enabled from /experimental. Restart Codex after enabling, then use /voice.",
},
default_enabled: false,
},
FeatureSpec {
Expand Down
8 changes: 7 additions & 1 deletion codex-rs/tui/src/bottom_pane/experimental_features_view.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Experimental controls with popup-owned discovery and configured enablement.
//! Failed saves retain intent for explicit retry; cancellation remains available.
//! Server voice discovery is gated by the client package's native runtime.

use crate::experimental_features::FeatureWriteResult;
use codex_app_server_protocol::ExperimentalFeature;
Expand Down Expand Up @@ -53,6 +54,7 @@ pub(crate) struct ExperimentalFeatureItem {

pub(crate) struct ExperimentalFeaturesView {
features: Vec<ExperimentalFeatureItem>,
voice_supported: bool,
initial_enabled: Vec<bool>,
unconfirmed: Vec<String>,
catalog_rx: Option<oneshot::Receiver<Result<Vec<ExperimentalFeature>, String>>>,
Expand Down Expand Up @@ -82,6 +84,7 @@ impl ExperimentalFeaturesView {
}
.to_string(),
thread_id,
voice_supported: codex_realtime_webrtc::RealtimeWebrtcSession::is_supported(),
write_rx: None,
catalog_rx,
unconfirmed: Vec::new(),
Expand Down Expand Up @@ -312,7 +315,10 @@ impl BottomPaneView for ExperimentalFeaturesView {
Ok(features) => {
let mut count = 0;
for feature in features {
if feature.stage != ExperimentalFeatureStage::Beta {
if feature.stage != ExperimentalFeatureStage::Beta
|| (feature.name == Feature::RealtimeConversation.key()
&& !self.voice_supported)
{
continue;
}
self.initial_enabled.push(feature.enabled);
Expand Down
29 changes: 29 additions & 0 deletions codex-rs/tui/src/bottom_pane/experimental_features_view_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,32 @@ fn snapshot_view(name: &str, view: &ExperimentalFeaturesView) {
view.render(area, &mut buffer);
insta::assert_snapshot!(name, buffer_text(&buffer));
}

#[test]
fn voice_discovery_requires_the_client_runtime() {
for supported in [false, true] {
let (app_tx, _app_rx) = tokio::sync::mpsc::unbounded_channel();
let (catalog_tx, catalog_rx) = oneshot::channel();
let mut view = ExperimentalFeaturesView::new(
Vec::new(),
ThreadId::new(),
Some(catalog_rx),
AppEventSender::new(app_tx),
crate::keymap::RuntimeKeymap::defaults().list,
);
view.voice_supported = supported;
catalog_tx
.send(Ok(vec![server_feature("realtime_conversation")]))
.unwrap();
assert!(view.pre_draw_tick(Instant::now()));
assert_eq!(view.features.len(), usize::from(supported));
snapshot_view(
if supported {
"voice_runtime_available"
} else {
"voice_runtime_unavailable"
},
&view,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
source: tui/src/bottom_pane/experimental_features_view_tests.rs
assertion_line: 208
expression: buffer_text(&buffer)
---

Experimental features
Checked features are configured on. Some experimental features take effect
only in new tasks or after restarting the Codex server.

› [x] Server realtime_conversation Description supplied by the server.

Press space to select or enter to save
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
source: tui/src/bottom_pane/experimental_features_view_tests.rs
assertion_line: 208
expression: buffer_text(&buffer)
---

Experimental features
Checked features are configured on. Some experimental features take effect
only in new tasks or after restarting the Codex server.
No server experiments available.

No experimental features available for now

Press space to select or enter to save
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
---
source: tui/src/chatwidget/tests/popups_and_settings.rs
assertion_line: 3083
expression: popup
---
Experimental features
Checked features are configured on. Some experimental features take effect
only in new tasks or after restarting the Codex server.

› [ ] JavaScript REPL Enable a persistent Node-backed JavaScript REPL for
interactive website debugging and other inline
JavaScript execution capabilities.
[x] Shell tool Allow the model to run shell commands.
[ ] Worktrees Create isolated Git worktrees and group sessions by
repository.
› [ ] JavaScript REPL Enable a persistent Node-backed JavaScript REPL for
interactive website debugging and other inline
JavaScript execution capabilities.
[x] Shell tool Allow the model to run shell commands.
[ ] Worktrees Create isolated Git worktrees and group sessions by
repository.
[ ] Voice conversations Talk with Codex using /voice.

Press space to select or enter to save
8 changes: 8 additions & 0 deletions codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3039,6 +3039,7 @@ async fn experimental_features_popup_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;

let worktrees = Feature::Worktrees.stage();
let voice = Feature::RealtimeConversation.stage();
let features = vec![
ExperimentalFeatureItem {
key: Feature::JsRepl.key().to_string(),
Expand All @@ -3061,6 +3062,13 @@ async fn experimental_features_popup_snapshot() {
description: worktrees.experimental_menu_description().unwrap().to_string(),
enabled: false,
},
ExperimentalFeatureItem {
key: Feature::RealtimeConversation.key().to_string(),
writable: true,
name: voice.experimental_menu_name().unwrap().to_string(),
description: voice.experimental_menu_description().unwrap().to_string(),
enabled: false,
},
];
let view = ExperimentalFeaturesView::new(
features,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
source: tui/src/tooltips.rs
assertion_line: 382
expression: "available.into_iter().filter(|tip| *tip ==\nvoice_tip).collect::<Vec<_>>().join(\"\\n\")"
---
NEW: Voice conversations can now be enabled from /experimental. Restart Codex after enabling, then use /voice.
35 changes: 33 additions & 2 deletions codex-rs/tui/src/tooltips.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use codex_features::FEATURES;
use codex_features::Feature;
use codex_protocol::account::PlanType;
use lazy_static::lazy_static;
use rand::Rng;
Expand Down Expand Up @@ -36,14 +37,17 @@ lazy_static! {
static ref ALL_TOOLTIPS: Vec<&'static str> = {
let mut tips = Vec::new();
tips.extend(TOOLTIPS.iter().copied());
tips.extend(experimental_tooltips());
tips.extend(experimental_tooltips(
codex_realtime_webrtc::RealtimeWebrtcSession::is_supported,
));
tips
};
}

fn experimental_tooltips() -> Vec<&'static str> {
fn experimental_tooltips(voice_supported: impl Fn() -> bool) -> Vec<&'static str> {
FEATURES
.iter()
.filter(|spec| spec.id != Feature::RealtimeConversation || voice_supported())
.filter_map(|spec| spec.stage.experimental_announcement())
.collect()
}
Expand Down Expand Up @@ -358,6 +362,33 @@ mod tests {
use rand::SeedableRng;
use rand::rngs::StdRng;

#[test]
fn experimental_voice_tooltip_requires_runtime_support() {
let unavailable = experimental_tooltips(|| false);
let available = experimental_tooltips(|| true);
let voice_tip = FEATURES
.iter()
.find(|spec| spec.id == Feature::RealtimeConversation)
.and_then(|spec| spec.stage.experimental_announcement())
.expect("voice has an experimental announcement");
assert_eq!(
unavailable,
available
.iter()
.copied()
.filter(|tip| *tip != voice_tip)
.collect::<Vec<_>>()
);
insta::assert_snapshot!(
"experimental_voice_tooltip",
available
.into_iter()
.filter(|tip| *tip == voice_tip)
.collect::<Vec<_>>()
.join("\n")
);
}

#[test]
fn random_tooltip_returns_some_tip_when_available() {
let mut rng = StdRng::seed_from_u64(42);
Expand Down
Loading