mirror of
https://github.com/openai/codex.git
synced 2026-11-08 16:38:30 +00:00
## What changed - Add feature-gated `/voice`, `/voice mute`, and `/voice stop` commands with local WebRTC audio and app-server signaling. - Show live transcripts, conversation status, and microphone and speaker levels. Preserve captions and undelivered answers across thread switches. - Speak final answers from voice handoffs while keeping delegated reasoning and commentary hidden and typed answers unspoken. - Retry eligible startup failures once and clean up voice sessions on thread switches and disconnects. - Stop voice and block late handoffs after a misalignment policy violation. Remove realtime event payloads and spoken text from receipt and debug logs. ## Testing Add coverage for voice command mapping, rejected starts, disconnect cleanup, delegated final-answer speech, late handoff rejection, and transcript and footer rendering. GitOrigin-RevId: 11def1f86b0e023b21d0d92072d6636ccad89815
105 lines
4.7 KiB
Rust
105 lines
4.7 KiB
Rust
//! Disconnects preserve editable input but never automatically retry queued submissions.
|
|
|
|
use super::*;
|
|
|
|
impl ChatWidget {
|
|
pub(crate) fn pause_for_disconnect(&mut self) {
|
|
// The app-server transport can fail while the separate WebRTC helper
|
|
// still sends microphone audio. Retire local media before showing offline UI.
|
|
let _ = self.reset_realtime_conversation();
|
|
if let Some(questions) = &mut self.bottom_pane.questions {
|
|
questions.delivery_enabled = false;
|
|
}
|
|
self.input_queue.recovered_queue = true;
|
|
self.input_queue.suppress_queue_autosend = true;
|
|
self.set_initial_user_message_submit_suppressed(/*suppressed*/ true);
|
|
if let Some(message) = self.initial_user_message.take() {
|
|
self.restore_user_message_to_composer(message);
|
|
}
|
|
self.bottom_pane.ensure_status_indicator();
|
|
self.bottom_pane
|
|
.set_interrupt_hint_visible(/*visible*/ false);
|
|
self.set_status_header("Reconnecting to app-server…".to_string());
|
|
self.set_footer_hint_override(Some(vec![("ctrl+c".into(), "quit".into())]));
|
|
self.add_error_message("Connection lost. Attempting to reconnect…".into());
|
|
}
|
|
|
|
/// Restore local input only after replay, which can otherwise move interrupted queues into the draft.
|
|
pub(crate) fn restore_reconnected_input(&mut self, input: Option<ThreadInputState>) {
|
|
let running = self.turn_lifecycle.agent_turn_running;
|
|
if let Some(mut input) = input {
|
|
// Its acceptance is unknown. Keep a local copy for manual recovery without
|
|
// comparing against partial history or automatically submitting it again.
|
|
if input.user_turn_pending_start
|
|
&& let Some(prompt) = input.safety_buffering_prompt.take()
|
|
{
|
|
input.queued_user_messages.push_front(prompt.into());
|
|
input
|
|
.queued_user_message_history_records
|
|
.push_front(UserMessageHistoryRecord::UserMessageText);
|
|
input.recovered_queue = true;
|
|
}
|
|
input.current_collaboration_mode = self.current_collaboration_mode.clone();
|
|
// Resume supplies model/effort, but not the user's selected collaboration mode.
|
|
if let Some(mask) = input.active_collaboration_mask.as_mut() {
|
|
mask.model = Some(self.current_model().to_string());
|
|
mask.reasoning_effort = Some(self.effective_reasoning_effort());
|
|
}
|
|
self.restore_thread_input_state(
|
|
Some(input),
|
|
ThreadInputStateRestoreMode {
|
|
preserve_in_flight_turn: false,
|
|
},
|
|
);
|
|
}
|
|
self.turn_lifecycle.restore_running(running, Instant::now());
|
|
self.update_task_running_state();
|
|
}
|
|
|
|
pub(crate) fn pause_unavailable_thread(&mut self) {
|
|
if let Some(questions) = &mut self.bottom_pane.questions {
|
|
questions.delivery_enabled = false;
|
|
}
|
|
self.turn_lifecycle
|
|
.restore_running(/*running*/ false, Instant::now());
|
|
self.update_task_running_state();
|
|
}
|
|
|
|
pub(crate) fn handle_disconnected_view_key(&mut self, key: KeyEvent) {
|
|
self.bottom_pane.handle_key_event(key);
|
|
}
|
|
|
|
pub(crate) fn handle_disconnected_key(&mut self, key: KeyEvent) {
|
|
if self.external_writer_view {
|
|
return;
|
|
}
|
|
if self.handle_question_key(key) {
|
|
return;
|
|
}
|
|
if key.kind == KeyEventKind::Press && self.chat_keymap.edit_queued_message.is_pressed(key) {
|
|
if let Some(composer) = self.pop_latest_queued_composer_state() {
|
|
self.restore_composer_state(composer);
|
|
} else if let Some(steer) = self.input_queue.pending_steers.pop_back() {
|
|
self.restore_user_message_to_composer(user_message_for_restore(
|
|
steer.user_message,
|
|
&steer.history_record,
|
|
));
|
|
self.input_queue.recovered_queue &= !self.input_queue.pending_steers.is_empty();
|
|
}
|
|
self.bottom_pane
|
|
.handle_disconnected_key(KeyEvent::new(KeyCode::Null, KeyModifiers::NONE));
|
|
self.refresh_pending_input_preview();
|
|
self.request_redraw();
|
|
} else {
|
|
self.bottom_pane.handle_disconnected_key(key);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn reconnect_failed(&mut self) {
|
|
self.set_status_header("Reconnect failed — check the endpoint, then relaunch".into());
|
|
self.add_error_message(
|
|
"Automatic reconnect could not restore this session. Your draft is still editable. Copy it before quitting with Ctrl+C, then reconnect with the same command.".into(),
|
|
);
|
|
}
|
|
}
|