Compare commits

..

1 Commits

Author SHA1 Message Date
Dylan Hurd
1f0aad9a66 fix: always reload auth 2025-09-23 10:24:43 -07:00
12 changed files with 48 additions and 153 deletions

View File

@@ -413,6 +413,7 @@ impl CodexMessageProcessor {
request_id: RequestId,
params: codex_protocol::mcp_protocol::GetAuthStatusParams,
) {
self.auth_manager.reload();
let include_token = params.include_token.unwrap_or(false);
let do_refresh = params.refresh_token.unwrap_or(false);

View File

@@ -269,11 +269,6 @@ impl ChatComposer {
self.textarea.text().to_string()
}
/// Get the current composer text (for runtime use).
pub(crate) fn text_content(&self) -> String {
self.textarea.text().to_string()
}
/// Attempt to start a burst by retro-capturing recent chars before the cursor.
pub fn attach_image(&mut self, path: PathBuf, width: u32, height: u32, format_label: &str) {
let placeholder = format!("[image {width}x{height} {format_label}]");
@@ -1287,7 +1282,7 @@ impl WidgetRef for ChatComposer {
} else {
key_hint::ctrl('J')
};
let mut base: Vec<Span<'static>> = vec![
vec![
key_hint::plain('⏎'),
" send ".into(),
newline_hint_key,
@@ -1296,14 +1291,7 @@ impl WidgetRef for ChatComposer {
" transcript ".into(),
key_hint::ctrl('C'),
" quit".into(),
];
// When there is text in the composer, show Ctrl+S to save as a custom prompt.
if !self.textarea.is_empty() {
base.push(" ".into());
base.push(key_hint::ctrl('S'));
base.push(" save prompt".into());
}
base
]
};
if !self.ctrl_c_quit_hint && self.esc_backtrack_hint {

View File

@@ -20,7 +20,7 @@ use super::textarea::TextArea;
use super::textarea::TextAreaState;
/// Callback invoked when the user submits a custom prompt.
pub(crate) type PromptSubmitted = Box<dyn Fn(String) -> bool + Send + Sync>;
pub(crate) type PromptSubmitted = Box<dyn Fn(String) + Send + Sync>;
/// Minimal multi-line text input view to collect custom review instructions.
pub(crate) struct CustomPromptView {
@@ -68,7 +68,8 @@ impl BottomPaneView for CustomPromptView {
..
} => {
let text = self.textarea.text().trim().to_string();
if !text.is_empty() && (self.on_submit)(text) {
if !text.is_empty() {
(self.on_submit)(text);
self.complete = true;
}
}

View File

@@ -360,11 +360,6 @@ impl BottomPane {
self.composer.is_empty()
}
/// Get the current composer text (without mutating state).
pub(crate) fn composer_text_now(&self) -> String {
self.composer.text_content()
}
pub(crate) fn is_task_running(&self) -> bool {
self.is_task_running
}

View File

@@ -11,4 +11,4 @@ expression: terminal.backend()
"▌ "
"▌ "
" "
"⏎ send ⌃J newline ⌃T transcript ⌃C quit ⌃S save prompt "
"⏎ send ⌃J newline ⌃T transcript ⌃C quit "

View File

@@ -11,4 +11,4 @@ expression: terminal.backend()
"▌ "
"▌ "
" "
"⏎ send ⌃J newline ⌃T transcript ⌃C quit ⌃S save prompt "
"⏎ send ⌃J newline ⌃T transcript ⌃C quit "

View File

@@ -11,4 +11,4 @@ expression: terminal.backend()
"▌ "
"▌ "
" "
"⏎ send ⌃J newline ⌃T transcript ⌃C quit ⌃S save prompt "
"⏎ send ⌃J newline ⌃T transcript ⌃C quit "

View File

@@ -11,4 +11,4 @@ expression: terminal.backend()
"▌ "
"▌ "
" "
"⏎ send ⌃J newline ⌃T transcript ⌃C quit ⌃S save prompt "
"⏎ send ⌃J newline ⌃T transcript ⌃C quit "

View File

@@ -54,7 +54,6 @@ use ratatui::layout::Layout;
use ratatui::layout::Rect;
use ratatui::widgets::Widget;
use ratatui::widgets::WidgetRef;
use std::fs;
use tokio::sync::mpsc::UnboundedSender;
use tracing::debug;
@@ -119,7 +118,7 @@ struct RunningCommand {
parsed_cmd: Vec<ParsedCommand>,
}
const RATE_LIMIT_WARNING_THRESHOLDS: [f64; 3] = [75.0, 90.0, 95.0];
const RATE_LIMIT_WARNING_THRESHOLDS: [f64; 3] = [50.0, 75.0, 90.0];
#[derive(Default)]
struct RateLimitWarningState {
@@ -135,30 +134,24 @@ impl RateLimitWarningState {
) -> Vec<String> {
let mut warnings = Vec::new();
let mut highest_secondary: Option<f64> = None;
while self.secondary_index < RATE_LIMIT_WARNING_THRESHOLDS.len()
&& secondary_used_percent >= RATE_LIMIT_WARNING_THRESHOLDS[self.secondary_index]
{
highest_secondary = Some(RATE_LIMIT_WARNING_THRESHOLDS[self.secondary_index]);
self.secondary_index += 1;
}
if let Some(threshold) = highest_secondary {
let threshold = RATE_LIMIT_WARNING_THRESHOLDS[self.secondary_index];
warnings.push(format!(
"Heads up, you've used over {threshold:.0}% of your weekly limit. Run /status for a breakdown."
));
self.secondary_index += 1;
}
let mut highest_primary: Option<f64> = None;
while self.primary_index < RATE_LIMIT_WARNING_THRESHOLDS.len()
&& primary_used_percent >= RATE_LIMIT_WARNING_THRESHOLDS[self.primary_index]
{
highest_primary = Some(RATE_LIMIT_WARNING_THRESHOLDS[self.primary_index]);
self.primary_index += 1;
}
if let Some(threshold) = highest_primary {
let threshold = RATE_LIMIT_WARNING_THRESHOLDS[self.primary_index];
warnings.push(format!(
"Heads up, you've used over {threshold:.0}% of your 5h limit. Run /status for a breakdown."
));
self.primary_index += 1;
}
warnings
@@ -316,57 +309,6 @@ impl ChatWidget {
self.request_redraw();
}
fn open_save_prompt_popup(&mut self) {
let tx = self.app_event_tx.clone();
let get_content = self.bottom_pane.composer_text_now();
let prompts_dir = self.config.codex_home.join("prompts");
let view = CustomPromptView::new(
"Save prompt".to_string(),
"Set custom prompt name".to_string(),
None,
Box::new(move |name: String| {
let content = get_content.clone();
let mut name_slug = slugify_prompt_name(&name);
if name_slug.is_empty() {
name_slug = "prompt".to_string();
}
if let Err(e) = fs::create_dir_all(&prompts_dir) {
tx.send(AppEvent::InsertHistoryCell(Box::new(
crate::history_cell::new_error_event(format!(
"Failed to create prompts dir: {e}"
)),
)));
return false;
}
let path = prompts_dir.join(format!("{name_slug}.md"));
if path.exists() {
tx.send(AppEvent::InsertHistoryCell(Box::new(
crate::history_cell::new_error_event(format!(
"Prompt \"{name}\" already exists; choose a different name."
)),
)));
return false;
}
if let Err(e) = fs::write(&path, content) {
tx.send(AppEvent::InsertHistoryCell(Box::new(
crate::history_cell::new_error_event(format!("Failed to save prompt: {e}")),
)));
return false;
}
// Informational message and refresh custom prompts list.
tx.send(AppEvent::InsertHistoryCell(Box::new(
crate::history_cell::new_info_event(
format!("Saved prompt as {}", path.display()),
None,
),
)));
tx.send(AppEvent::CodexOp(Op::ListCustomPrompts));
true
}),
);
self.bottom_pane.show_view(Box::new(view));
}
fn on_reasoning_section_break(&mut self) {
// Start a new reasoning block for header extraction and accumulate transcript.
self.full_reasoning_buffer.push_str(&self.reasoning_buffer);
@@ -951,17 +893,6 @@ impl ChatWidget {
}
return;
}
KeyEvent {
code: KeyCode::Char('s'),
modifiers: KeyModifiers::CONTROL,
kind: KeyEventKind::Press,
..
} => {
if !self.bottom_pane.composer_is_empty() {
self.open_save_prompt_popup();
}
return;
}
other if other.kind == KeyEventKind::Press => {
self.bottom_pane.clear_ctrl_c_quit_hint();
}
@@ -1881,7 +1812,7 @@ impl ChatWidget {
Box::new(move |prompt: String| {
let trimmed = prompt.trim().to_string();
if trimmed.is_empty() {
return false;
return;
}
tx.send(AppEvent::CodexOp(Op::Review {
review_request: ReviewRequest {
@@ -1889,7 +1820,6 @@ impl ChatWidget {
user_facing_hint: trimmed,
},
}));
true
}),
);
self.bottom_pane.show_view(Box::new(view));
@@ -2051,29 +1981,6 @@ fn extract_first_bold(s: &str) -> Option<String> {
None
}
fn slugify_prompt_name(name: &str) -> String {
let mut out = String::new();
let mut last_dash = false;
for ch in name.trim().chars() {
if ch.is_ascii_alphanumeric() {
out.push(ch.to_ascii_lowercase());
last_dash = false;
} else if ch == '-' || ch == '_' || ch.is_whitespace() {
if !last_dash {
out.push('-');
last_dash = true;
}
} else {
// skip other characters
if !last_dash {
out.push('-');
last_dash = true;
}
}
}
out.trim_matches('-').to_string()
}
#[cfg(test)]
pub(crate) fn show_review_commit_picker_with_entries(
chat: &mut ChatWidget,

View File

@@ -13,4 +13,4 @@ expression: visual
▌ Summarize recent commits
⏎ send ⌃J newline ⌃T transcript ⌃C quit ⌃S save prompt
⏎ send ⌃J newline ⌃T transcript ⌃C quit

View File

@@ -392,22 +392,33 @@ fn rate_limit_warnings_emit_thresholds() {
warnings.extend(state.take_warnings(95.0, 10.0));
assert_eq!(
warnings,
vec![
String::from(
"Heads up, you've used over 75% of your 5h limit. Run /status for a breakdown."
),
String::from(
"Heads up, you've used over 75% of your weekly limit. Run /status for a breakdown.",
),
String::from(
"Heads up, you've used over 95% of your 5h limit. Run /status for a breakdown."
),
String::from(
"Heads up, you've used over 95% of your weekly limit. Run /status for a breakdown.",
),
],
"expected one warning per limit for the highest crossed threshold"
warnings.len(),
6,
"expected one warning per threshold per limit"
);
assert!(
warnings
.iter()
.any(|w| w.contains("Heads up, you've used over 50% of your 5h limit.")),
"expected hourly 50% warning (new copy)"
);
assert!(
warnings
.iter()
.any(|w| w.contains("Heads up, you've used over 50% of your weekly limit.")),
"expected weekly 50% warning (new copy)"
);
assert!(
warnings
.iter()
.any(|w| w.contains("Heads up, you've used over 90% of your 5h limit.")),
"expected hourly 90% warning (new copy)"
);
assert!(
warnings
.iter()
.any(|w| w.contains("Heads up, you've used over 90% of your weekly limit.")),
"expected weekly 90% warning (new copy)"
);
}

View File

@@ -1175,13 +1175,7 @@ pub(crate) fn new_status_output(
// 👤 Account (only if ChatGPT tokens exist), shown under the first block
let auth_file = get_auth_file(&config.codex_home);
let auth = try_read_auth_json(&auth_file).ok();
let is_chatgpt_auth = auth
.as_ref()
.and_then(|auth| auth.tokens.as_ref())
.is_some();
if is_chatgpt_auth
&& let Some(auth) = auth.as_ref()
if let Ok(auth) = try_read_auth_json(&auth_file)
&& let Some(tokens) = auth.tokens.clone()
{
lines.push(vec![padded_emoji("👤").into(), "Account".bold()].into());
@@ -1257,10 +1251,8 @@ pub(crate) fn new_status_output(
format_with_separators(usage.blended_total()).into(),
]));
if is_chatgpt_auth {
lines.push("".into());
lines.extend(build_status_limit_lines(rate_limits));
}
lines.push("".into());
lines.extend(build_status_limit_lines(rate_limits));
PlainHistoryCell { lines }
}
@@ -1638,7 +1630,7 @@ fn build_status_limit_lines(snapshot: Option<&RateLimitSnapshotEvent>) -> Vec<Li
lines.push(build_status_limit_line(&label, percent, label_width));
}
}
None => lines.push("Send a message to load usage data.".into()),
None => lines.push("Rate limit data not available yet.".dim().into()),
}
lines