mirror of
https://github.com/openai/codex.git
synced 2026-02-01 22:47:52 +00:00
Compare commits
1 Commits
1271d450b1
...
codex/set-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6e74b2c08 |
@@ -42,6 +42,7 @@ use codex_protocol::ConversationId;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::openai_models::ModelUpgrade;
|
||||
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use color_eyre::eyre::Result;
|
||||
use color_eyre::eyre::WrapErr;
|
||||
use crossterm::event::KeyCode;
|
||||
@@ -66,6 +67,8 @@ use tokio::sync::mpsc::unbounded_channel;
|
||||
use crate::history_cell::UpdateAvailableHistoryCell;
|
||||
|
||||
const EXTERNAL_EDITOR_HINT: &str = "Save and close external editor to continue.";
|
||||
const TERMINAL_TITLE_INSTRUCTIONS: &str =
|
||||
"Generate a short title (max 4 words) for the request. Respond with the title only.";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AppExitInfo {
|
||||
@@ -91,6 +94,72 @@ fn session_summary(
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_terminal_title(raw: &str) -> Option<String> {
|
||||
let trimmed = raw.trim();
|
||||
if trimmed.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let first_line = trimmed.lines().next().unwrap_or(trimmed);
|
||||
let stripped = first_line.trim_matches(|ch| matches!(ch, '"' | '\'' | '`'));
|
||||
let words: Vec<&str> = stripped.split_whitespace().collect();
|
||||
if words.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let title = words.into_iter().take(4).collect::<Vec<_>>().join(" ");
|
||||
let title = title.trim_matches(|ch| matches!(ch, '"' | '\'' | '`'));
|
||||
if title.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(title.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
async fn generate_terminal_title(
|
||||
server: Arc<ConversationManager>,
|
||||
mut config: Config,
|
||||
model: String,
|
||||
request: String,
|
||||
) -> Option<String> {
|
||||
config.model = Some(model);
|
||||
config.base_instructions = Some(TERMINAL_TITLE_INSTRUCTIONS.to_string());
|
||||
config.user_instructions = None;
|
||||
config.project_doc_max_bytes = 0;
|
||||
|
||||
let new_conversation = server.new_conversation(config).await.ok()?;
|
||||
let conversation_id = new_conversation.conversation_id;
|
||||
let conversation = new_conversation.conversation;
|
||||
|
||||
let prompt = format!(
|
||||
"Create a concise title (max 4 words) for this request. Respond with only the title.\n\nRequest:\n{request}"
|
||||
);
|
||||
conversation
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text { text: prompt }],
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
let mut output = None;
|
||||
loop {
|
||||
let event = conversation.next_event().await.ok()?;
|
||||
match event.msg {
|
||||
EventMsg::TaskComplete(task_complete) => {
|
||||
output = task_complete.last_agent_message;
|
||||
break;
|
||||
}
|
||||
EventMsg::TurnAborted(_) => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = conversation.submit(Op::Shutdown).await;
|
||||
let _ = server.remove_conversation(&conversation_id).await;
|
||||
|
||||
output.and_then(|title| normalize_terminal_title(&title))
|
||||
}
|
||||
|
||||
fn errors_for_cwd(cwd: &Path, response: &ListSkillsResponseEvent) -> Vec<SkillErrorInfo> {
|
||||
response
|
||||
.skills
|
||||
@@ -711,6 +780,24 @@ impl App {
|
||||
AppEvent::CommitTick => {
|
||||
self.chat_widget.on_commit_tick();
|
||||
}
|
||||
AppEvent::GenerateTerminalTitle { request } => {
|
||||
let server = self.server.clone();
|
||||
let config = self.config.clone();
|
||||
let model = self.current_model.clone();
|
||||
let tx = self.app_event_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Some(title) =
|
||||
generate_terminal_title(server, config, model, request).await
|
||||
{
|
||||
tx.send(AppEvent::SetTerminalTitle(title));
|
||||
}
|
||||
});
|
||||
}
|
||||
AppEvent::SetTerminalTitle(title) => {
|
||||
if let Err(err) = tui.set_terminal_title(&title) {
|
||||
tracing::warn!("failed to set terminal title: {err}");
|
||||
}
|
||||
}
|
||||
AppEvent::CodexEvent(event) => {
|
||||
if self.suppress_shutdown_complete
|
||||
&& matches!(event.msg, EventMsg::ShutdownComplete)
|
||||
|
||||
@@ -52,6 +52,14 @@ pub(crate) enum AppEvent {
|
||||
/// Result of computing a `/diff` command.
|
||||
DiffResult(String),
|
||||
|
||||
/// Generate a short terminal title from the initial user request.
|
||||
GenerateTerminalTitle {
|
||||
request: String,
|
||||
},
|
||||
|
||||
/// Set the terminal title to the provided text.
|
||||
SetTerminalTitle(String),
|
||||
|
||||
InsertHistoryCell(Box<dyn HistoryCell>),
|
||||
|
||||
StartCommitAnimation,
|
||||
|
||||
@@ -368,6 +368,7 @@ pub(crate) struct ChatWidget {
|
||||
// Current session rollout path (if known)
|
||||
current_rollout_path: Option<PathBuf>,
|
||||
external_editor_state: ExternalEditorState,
|
||||
terminal_title_requested: bool,
|
||||
}
|
||||
|
||||
struct UserMessage {
|
||||
@@ -1478,6 +1479,7 @@ impl ChatWidget {
|
||||
feedback,
|
||||
current_rollout_path: None,
|
||||
external_editor_state: ExternalEditorState::Closed,
|
||||
terminal_title_requested: false,
|
||||
};
|
||||
|
||||
widget.prefetch_rate_limits();
|
||||
@@ -1564,6 +1566,7 @@ impl ChatWidget {
|
||||
feedback,
|
||||
current_rollout_path: None,
|
||||
external_editor_state: ExternalEditorState::Closed,
|
||||
terminal_title_requested: false,
|
||||
};
|
||||
|
||||
widget.prefetch_rate_limits();
|
||||
@@ -1944,6 +1947,13 @@ impl ChatWidget {
|
||||
items.push(UserInput::LocalImage { path });
|
||||
}
|
||||
|
||||
if !self.terminal_title_requested && !text.is_empty() {
|
||||
self.terminal_title_requested = true;
|
||||
self.app_event_tx.send(AppEvent::GenerateTerminalTitle {
|
||||
request: text.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(skills) = self.bottom_pane.skills() {
|
||||
let skill_mentions = find_skill_mentions(&text, skills);
|
||||
for skill in skill_mentions {
|
||||
|
||||
@@ -408,6 +408,7 @@ async fn make_chatwidget_manual(
|
||||
feedback: codex_feedback::CodexFeedback::new(),
|
||||
current_rollout_path: None,
|
||||
external_editor_state: ExternalEditorState::Closed,
|
||||
terminal_title_requested: false,
|
||||
};
|
||||
(widget, rx, op_rx)
|
||||
}
|
||||
|
||||
@@ -120,6 +120,27 @@ impl Command for DisableAlternateScroll {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct SetTerminalTitle(pub String);
|
||||
|
||||
impl Command for SetTerminalTitle {
|
||||
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
||||
write!(f, "\x1b]0;{}\x07", self.0)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn execute_winapi(&self) -> Result<()> {
|
||||
Err(std::io::Error::other(
|
||||
"tried to execute SetTerminalTitle using WinAPI; use ANSI instead",
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn is_ansi_code_supported(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn restore_common(should_disable_raw_mode: bool) -> Result<()> {
|
||||
// Pop may fail on platforms that didn't support the push; ignore errors.
|
||||
let _ = execute!(stdout(), PopKeyboardEnhancementFlags);
|
||||
@@ -285,6 +306,13 @@ impl Tui {
|
||||
self.enhanced_keys_supported
|
||||
}
|
||||
|
||||
pub fn set_terminal_title(&mut self, title: &str) -> Result<()> {
|
||||
execute!(
|
||||
self.terminal.backend_mut(),
|
||||
SetTerminalTitle(title.to_string())
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_alt_screen_active(&self) -> bool {
|
||||
self.alt_screen_active.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user