mirror of
https://github.com/openai/codex.git
synced 2026-06-01 19:02:59 +00:00
remote tasks
This commit is contained in:
@@ -99,6 +99,8 @@ pub(crate) struct ChatComposer {
|
||||
// When true, disables paste-burst logic and inserts characters immediately.
|
||||
disable_paste_burst: bool,
|
||||
custom_prompts: Vec<CustomPrompt>,
|
||||
// Optional override for footer hint items.
|
||||
footer_hint_override: Option<Vec<(String, String)>>,
|
||||
}
|
||||
|
||||
/// Popup state – at most one can be visible at any time.
|
||||
@@ -137,6 +139,7 @@ impl ChatComposer {
|
||||
paste_burst: PasteBurst::default(),
|
||||
disable_paste_burst: false,
|
||||
custom_prompts: Vec::new(),
|
||||
footer_hint_override: None,
|
||||
};
|
||||
// Apply configuration via the setter to keep side-effects centralized.
|
||||
this.set_disable_paste_burst(disable_paste_burst);
|
||||
@@ -242,6 +245,10 @@ impl ChatComposer {
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn set_footer_hint_override(&mut self, items: Option<Vec<(String, String)>>) {
|
||||
self.footer_hint_override = items;
|
||||
}
|
||||
|
||||
pub fn handle_paste_image_path(&mut self, pasted: String) -> bool {
|
||||
let Some(path_buf) = normalize_pasted_path(&pasted) else {
|
||||
return false;
|
||||
@@ -1266,6 +1273,17 @@ impl WidgetRef for ChatComposer {
|
||||
"Ctrl+C again".set_style(key_hint_style),
|
||||
" to quit".into(),
|
||||
]
|
||||
} else if let Some(items) = &self.footer_hint_override {
|
||||
let mut out: Vec<Span> = Vec::new();
|
||||
for (i, (key, label)) in items.iter().enumerate() {
|
||||
out.push(Span::from(" "));
|
||||
out.push(key.as_str().set_style(key_hint_style));
|
||||
out.push(Span::from(format!(" {label}")));
|
||||
if i + 1 != items.len() {
|
||||
out.push(Span::from(" "));
|
||||
}
|
||||
}
|
||||
out
|
||||
} else {
|
||||
let newline_hint_key = if self.use_shift_enter_hint {
|
||||
"Shift+⏎"
|
||||
|
||||
@@ -26,7 +26,7 @@ mod paste_burst;
|
||||
mod popup_consts;
|
||||
mod scroll_state;
|
||||
mod selection_popup_common;
|
||||
mod textarea;
|
||||
pub(crate) mod textarea;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum CancellationEvent {
|
||||
|
||||
@@ -1076,6 +1076,14 @@ pub(crate) fn new_mcp_tools_output(
|
||||
lines.push(vec![" • Command: ".into(), cmd_display.into()].into());
|
||||
}
|
||||
|
||||
if let Some(env) = cfg.env.as_ref()
|
||||
&& !env.is_empty()
|
||||
{
|
||||
let mut env_pairs: Vec<String> = env.iter().map(|(k, v)| format!("{k}={v}")).collect();
|
||||
env_pairs.sort();
|
||||
lines.push(vec![" • Env: ".into(), env_pairs.join(" ").into()].into());
|
||||
}
|
||||
|
||||
if names.is_empty() {
|
||||
lines.push(" • Tools: (none)".into());
|
||||
} else {
|
||||
|
||||
@@ -46,6 +46,7 @@ mod markdown;
|
||||
mod markdown_stream;
|
||||
pub mod onboarding;
|
||||
mod pager_overlay;
|
||||
pub mod public_widgets;
|
||||
mod render;
|
||||
mod session_log;
|
||||
mod shimmer;
|
||||
@@ -65,6 +66,8 @@ mod chatwidget_stream_tests;
|
||||
mod updates;
|
||||
|
||||
pub use cli::Cli;
|
||||
pub use public_widgets::composer_input::ComposerAction;
|
||||
pub use public_widgets::composer_input::ComposerInput;
|
||||
|
||||
use crate::onboarding::TrustDirectorySelection;
|
||||
use crate::onboarding::onboarding_screen::OnboardingScreenArgs;
|
||||
@@ -312,11 +315,13 @@ async fn run_ratatui_app(
|
||||
if should_show_onboarding {
|
||||
let directory_trust_decision = run_onboarding_app(
|
||||
OnboardingScreenArgs {
|
||||
codex_home: config.codex_home.clone(),
|
||||
cwd: config.cwd.clone(),
|
||||
show_login_screen: should_show_login_screen(login_status, &config),
|
||||
show_trust_screen: should_show_trust_screen,
|
||||
login_status,
|
||||
preferred_auth_method: config.preferred_auth_method,
|
||||
auth_manager: auth_manager.clone(),
|
||||
config: config.clone(),
|
||||
},
|
||||
&mut tui,
|
||||
)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
use codex_core::AuthManager;
|
||||
use codex_core::auth::CLIENT_ID;
|
||||
use codex_core::config::Config;
|
||||
use codex_login::ServerOptions;
|
||||
use codex_login::ShutdownHandle;
|
||||
use codex_login::run_login_server;
|
||||
@@ -114,7 +113,6 @@ pub(crate) struct AuthModeWidget {
|
||||
pub login_status: LoginStatus,
|
||||
pub preferred_auth_method: AuthMode,
|
||||
pub auth_manager: Arc<AuthManager>,
|
||||
pub config: Config,
|
||||
}
|
||||
|
||||
impl AuthModeWidget {
|
||||
@@ -316,11 +314,7 @@ impl AuthModeWidget {
|
||||
}
|
||||
|
||||
self.error = None;
|
||||
let opts = ServerOptions::new(
|
||||
self.codex_home.clone(),
|
||||
CLIENT_ID.to_string(),
|
||||
self.config.responses_originator_header.clone(),
|
||||
);
|
||||
let opts = ServerOptions::new(self.codex_home.clone(), CLIENT_ID.to_string());
|
||||
match run_login_server(opts) {
|
||||
Ok(child) => {
|
||||
let sign_in_state = self.sign_in_state.clone();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use codex_core::AuthManager;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::git_info::get_git_repo_root;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
@@ -22,6 +21,7 @@ use crate::tui::FrameRequester;
|
||||
use crate::tui::Tui;
|
||||
use crate::tui::TuiEvent;
|
||||
use color_eyre::eyre::Result;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
|
||||
@@ -53,25 +53,26 @@ pub(crate) struct OnboardingScreen {
|
||||
}
|
||||
|
||||
pub(crate) struct OnboardingScreenArgs {
|
||||
pub codex_home: PathBuf,
|
||||
pub cwd: PathBuf,
|
||||
pub show_trust_screen: bool,
|
||||
pub show_login_screen: bool,
|
||||
pub login_status: LoginStatus,
|
||||
pub preferred_auth_method: AuthMode,
|
||||
pub auth_manager: Arc<AuthManager>,
|
||||
pub config: Config,
|
||||
}
|
||||
|
||||
impl OnboardingScreen {
|
||||
pub(crate) fn new(tui: &mut Tui, args: OnboardingScreenArgs) -> Self {
|
||||
let OnboardingScreenArgs {
|
||||
codex_home,
|
||||
cwd,
|
||||
show_trust_screen,
|
||||
show_login_screen,
|
||||
login_status,
|
||||
preferred_auth_method,
|
||||
auth_manager,
|
||||
config,
|
||||
} = args;
|
||||
let preferred_auth_method = config.preferred_auth_method;
|
||||
let cwd = config.cwd.clone();
|
||||
let codex_home = config.codex_home.clone();
|
||||
let mut steps: Vec<Step> = vec![Step::Welcome(WelcomeWidget {
|
||||
is_logged_in: !matches!(login_status, LoginStatus::NotAuthenticated),
|
||||
})];
|
||||
@@ -83,9 +84,8 @@ impl OnboardingScreen {
|
||||
sign_in_state: Arc::new(RwLock::new(SignInState::PickMode)),
|
||||
codex_home: codex_home.clone(),
|
||||
login_status,
|
||||
auth_manager,
|
||||
preferred_auth_method,
|
||||
config,
|
||||
auth_manager,
|
||||
}))
|
||||
}
|
||||
let is_git_repo = get_git_repo_root(&cwd).is_some();
|
||||
|
||||
95
codex-rs/tui/src/public_widgets/composer_input.rs
Normal file
95
codex-rs/tui/src/public_widgets/composer_input.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
//! Public wrapper around the internal ChatComposer for simple, reusable text input.
|
||||
//!
|
||||
//! This exposes a minimal interface suitable for other crates (e.g.,
|
||||
//! codex-cloud-tasks) to reuse the mature composer behavior: multi-line input,
|
||||
//! paste heuristics, Enter-to-submit, and Shift+Enter for newline.
|
||||
|
||||
use crossterm::event::KeyEvent;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::widgets::WidgetRef;
|
||||
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::bottom_pane::ChatComposer;
|
||||
use crate::bottom_pane::InputResult;
|
||||
|
||||
/// Action returned from feeding a key event into the ComposerInput.
|
||||
pub enum ComposerAction {
|
||||
/// The user submitted the current text (typically via Enter). Contains the submitted text.
|
||||
Submitted(String),
|
||||
/// No submission occurred; UI may need to redraw if `needs_redraw()` returned true.
|
||||
None,
|
||||
}
|
||||
|
||||
/// A minimal, public wrapper for the internal `ChatComposer` that behaves as a
|
||||
/// reusable text input field with submit semantics.
|
||||
pub struct ComposerInput {
|
||||
inner: ChatComposer,
|
||||
_tx: tokio::sync::mpsc::UnboundedSender<AppEvent>,
|
||||
}
|
||||
|
||||
impl ComposerInput {
|
||||
/// Create a new composer input with a neutral placeholder.
|
||||
pub fn new() -> Self {
|
||||
let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let sender = AppEventSender::new(tx.clone());
|
||||
// `enhanced_keys_supported=true` enables Shift+Enter newline hint/behavior.
|
||||
let inner = ChatComposer::new(true, sender, true, "Compose new task".to_string(), false);
|
||||
Self { inner, _tx: tx }
|
||||
}
|
||||
|
||||
/// Returns true if the input is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.inner.is_empty()
|
||||
}
|
||||
|
||||
/// Clear the input text.
|
||||
pub fn clear(&mut self) {
|
||||
self.inner.set_text_content(String::new());
|
||||
}
|
||||
|
||||
/// Feed a key event into the composer and return a high-level action.
|
||||
pub fn input(&mut self, key: KeyEvent) -> ComposerAction {
|
||||
match self.inner.handle_key_event(key).0 {
|
||||
InputResult::Submitted(text) => ComposerAction::Submitted(text),
|
||||
_ => ComposerAction::None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Override the footer hint items displayed under the composer.
|
||||
/// Each tuple is rendered as "<key> <label>", with keys styled.
|
||||
pub fn set_hint_items(&mut self, items: Vec<(impl Into<String>, impl Into<String>)>) {
|
||||
let mapped: Vec<(String, String)> = items
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.into(), v.into()))
|
||||
.collect();
|
||||
self.inner.set_footer_hint_override(Some(mapped));
|
||||
}
|
||||
|
||||
/// Clear any previously set custom hint items and restore the default hints.
|
||||
pub fn clear_hint_items(&mut self) {
|
||||
self.inner.set_footer_hint_override(None);
|
||||
}
|
||||
|
||||
/// Desired height (in rows) for a given width.
|
||||
pub fn desired_height(&self, width: u16) -> u16 {
|
||||
self.inner.desired_height(width)
|
||||
}
|
||||
|
||||
/// Compute the on-screen cursor position for the given area.
|
||||
pub fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
|
||||
self.inner.cursor_pos(area)
|
||||
}
|
||||
|
||||
/// Render the input into the provided buffer at `area`.
|
||||
pub fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
WidgetRef::render_ref(&self.inner, area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ComposerInput {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
2
codex-rs/tui/src/public_widgets/mod.rs
Normal file
2
codex-rs/tui/src/public_widgets/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod composer_input;
|
||||
pub mod text_input;
|
||||
83
codex-rs/tui/src/public_widgets/text_input.rs
Normal file
83
codex-rs/tui/src/public_widgets/text_input.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
//! Public, minimal wrapper around the internal multiline TextArea widget.
|
||||
//!
|
||||
//! This exposes a stable, crate-agnostic text input for other Codex crates
|
||||
//! (e.g., cloud-tasks) without making the whole TextArea API public.
|
||||
|
||||
use crossterm::event::KeyEvent;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::widgets::StatefulWidgetRef;
|
||||
|
||||
// Use the internal text area implementation.
|
||||
use crate::bottom_pane::textarea::TextArea as InnerTextArea;
|
||||
use crate::bottom_pane::textarea::TextAreaState as InnerTextAreaState;
|
||||
|
||||
use std::cell::RefCell;
|
||||
|
||||
/// A reusable, multiline text input field with wrapping and cursor movement.
|
||||
///
|
||||
/// This wrapper intentionally exposes a very small surface area needed by
|
||||
/// external consumers, while delegating to codex-tui's internal TextArea for
|
||||
/// behavior and rendering.
|
||||
pub struct TextInput {
|
||||
ta: InnerTextArea,
|
||||
state: RefCell<InnerTextAreaState>,
|
||||
}
|
||||
|
||||
impl Default for TextInput {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl TextInput {
|
||||
/// Create a new, empty input.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
ta: InnerTextArea::new(),
|
||||
state: RefCell::new(InnerTextAreaState::default()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the input contents.
|
||||
pub fn set_text(&mut self, text: &str) {
|
||||
self.ta.set_text(text);
|
||||
}
|
||||
|
||||
/// Return the current text contents.
|
||||
pub fn text(&self) -> &str {
|
||||
self.ta.text()
|
||||
}
|
||||
|
||||
/// Clear the input.
|
||||
pub fn clear(&mut self) {
|
||||
self.ta.set_text("");
|
||||
}
|
||||
|
||||
/// Returns true if the input is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.ta.is_empty()
|
||||
}
|
||||
|
||||
/// Handle a key event (inserts characters, moves cursor, etc.).
|
||||
pub fn input(&mut self, key: KeyEvent) {
|
||||
self.ta.input(key);
|
||||
}
|
||||
|
||||
/// Desired height (in rows) for a given width.
|
||||
pub fn desired_height(&self, width: u16) -> u16 {
|
||||
self.ta.desired_height(width)
|
||||
}
|
||||
|
||||
/// Compute the on-screen cursor position for the given area.
|
||||
pub fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
|
||||
let state = self.state.borrow();
|
||||
self.ta.cursor_pos_with_state(area, &state)
|
||||
}
|
||||
|
||||
/// Render the input into the provided buffer at `area`.
|
||||
pub fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
StatefulWidgetRef::render_ref(&(&self.ta), area, buf, &mut state);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user