mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
merge main into code-branch-20250926-063412 before merge
This commit is contained in:
Generated
+451
-334
File diff suppressed because it is too large
Load Diff
@@ -72,6 +72,7 @@ clap = "4"
|
||||
clap_complete = "4"
|
||||
color-eyre = "0.6.3"
|
||||
crossterm = "0.28.1"
|
||||
ctor = "0.5.0"
|
||||
derive_more = "2"
|
||||
diffy = "0.4.2"
|
||||
dirs = "6"
|
||||
|
||||
@@ -29,6 +29,7 @@ codex-protocol = { workspace = true }
|
||||
codex-protocol-ts = { workspace = true }
|
||||
codex-tui = { workspace = true }
|
||||
codex-version = { path = "../codex-version" }
|
||||
ctor = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "gzip", "json"] }
|
||||
regex = "1"
|
||||
@@ -52,6 +53,15 @@ tokio = { workspace = true, features = [
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
libc = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
libc = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
libc = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = { workspace = true }
|
||||
predicates = { workspace = true }
|
||||
|
||||
@@ -15,6 +15,7 @@ use codex_cli::login::run_login_with_chatgpt;
|
||||
use codex_cli::login::run_logout;
|
||||
use codex_cli::proto;
|
||||
mod llm;
|
||||
mod pre_main_hardening;
|
||||
use llm::{LlmCli, run_llm};
|
||||
use codex_common::CliConfigOverrides;
|
||||
use codex_core::find_conversation_path_by_id_str;
|
||||
@@ -32,6 +33,33 @@ use crate::mcp_cmd::McpCli;
|
||||
use crate::proto::ProtoCli;
|
||||
|
||||
const CLI_COMMAND_NAME: &str = "code";
|
||||
pub(crate) const CODEX_SECURE_MODE_ENV_VAR: &str = "CODEX_SECURE_MODE";
|
||||
|
||||
/// As early as possible in the process lifecycle, apply hardening measures
|
||||
/// if the CODEX_SECURE_MODE environment variable is set to "1".
|
||||
#[ctor::ctor]
|
||||
fn pre_main_hardening() {
|
||||
let secure_mode = match std::env::var(CODEX_SECURE_MODE_ENV_VAR) {
|
||||
Ok(value) => value,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
if secure_mode == "1" {
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
crate::pre_main_hardening::pre_main_hardening_linux();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
crate::pre_main_hardening::pre_main_hardening_macos();
|
||||
|
||||
#[cfg(windows)]
|
||||
crate::pre_main_hardening::pre_main_hardening_windows();
|
||||
}
|
||||
|
||||
// Always clear this env var so child processes don't inherit it.
|
||||
unsafe {
|
||||
std::env::remove_var(CODEX_SECURE_MODE_ENV_VAR);
|
||||
}
|
||||
}
|
||||
|
||||
/// Codex CLI
|
||||
///
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
const PRCTL_FAILED_EXIT_CODE: i32 = 5;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
const PTRACE_DENY_ATTACH_FAILED_EXIT_CODE: i32 = 6;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))]
|
||||
const SET_RLIMIT_CORE_FAILED_EXIT_CODE: i32 = 7;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub(crate) fn pre_main_hardening_linux() {
|
||||
// Disable ptrace attach / mark process non-dumpable.
|
||||
let ret_code = unsafe { libc::prctl(libc::PR_SET_DUMPABLE, 0, 0, 0, 0) };
|
||||
if ret_code != 0 {
|
||||
eprintln!(
|
||||
"ERROR: prctl(PR_SET_DUMPABLE, 0) failed: {}",
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
std::process::exit(PRCTL_FAILED_EXIT_CODE);
|
||||
}
|
||||
|
||||
// For "defense in depth," set the core file size limit to 0.
|
||||
set_core_file_size_limit_to_zero();
|
||||
|
||||
// Official Codex releases are MUSL-linked, which means that variables such
|
||||
// as LD_PRELOAD are ignored anyway, but just to be sure, clear them here.
|
||||
let ld_keys: Vec<String> = std::env::vars()
|
||||
.filter_map(|(key, _)| key.starts_with("LD_").then_some(key))
|
||||
.collect();
|
||||
|
||||
for key in ld_keys {
|
||||
unsafe {
|
||||
std::env::remove_var(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) fn pre_main_hardening_macos() {
|
||||
// Prevent debuggers from attaching to this process.
|
||||
let ret_code = unsafe { libc::ptrace(libc::PT_DENY_ATTACH, 0, std::ptr::null_mut(), 0) };
|
||||
if ret_code == -1 {
|
||||
eprintln!(
|
||||
"ERROR: ptrace(PT_DENY_ATTACH) failed: {}",
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
std::process::exit(PTRACE_DENY_ATTACH_FAILED_EXIT_CODE);
|
||||
}
|
||||
|
||||
// Set the core file size limit to 0 to prevent core dumps.
|
||||
set_core_file_size_limit_to_zero();
|
||||
|
||||
// Remove all DYLD_ environment variables, which can be used to subvert
|
||||
// library loading.
|
||||
let dyld_keys: Vec<String> = std::env::vars()
|
||||
.filter_map(|(key, _)| key.starts_with("DYLD_").then_some(key))
|
||||
.collect();
|
||||
|
||||
for key in dyld_keys {
|
||||
unsafe {
|
||||
std::env::remove_var(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn set_core_file_size_limit_to_zero() {
|
||||
let rlim = libc::rlimit {
|
||||
rlim_cur: 0,
|
||||
rlim_max: 0,
|
||||
};
|
||||
|
||||
let ret_code = unsafe { libc::setrlimit(libc::RLIMIT_CORE, &rlim) };
|
||||
if ret_code != 0 {
|
||||
eprintln!(
|
||||
"ERROR: setrlimit(RLIMIT_CORE) failed: {}",
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
std::process::exit(SET_RLIMIT_CORE_FAILED_EXIT_CODE);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub(crate) fn pre_main_hardening_windows() {
|
||||
// TODO: Perform the appropriate configuration for Windows.
|
||||
}
|
||||
+62
-41
@@ -243,52 +243,73 @@ impl App<'_> {
|
||||
// back off to reduce CPU when idle.
|
||||
let hot_typing = Instant::now().duration_since(last_key_time) <= Duration::from_millis(250);
|
||||
let poll_timeout = if hot_typing { Duration::from_millis(2) } else { Duration::from_millis(10) };
|
||||
if let Ok(true) = crossterm::event::poll(poll_timeout) {
|
||||
if let Ok(event) = crossterm::event::read() {
|
||||
match event {
|
||||
crossterm::event::Event::Key(key_event) => {
|
||||
// Some Windows terminals (e.g., legacy conhost) only report
|
||||
// `Release` events when keyboard enhancement flags are not
|
||||
// supported. Preserve those events so onboarding works there.
|
||||
if !drop_release_events
|
||||
|| matches!(key_event.kind, KeyEventKind::Press | KeyEventKind::Repeat)
|
||||
{
|
||||
last_key_time = Instant::now();
|
||||
app_event_tx.send(AppEvent::KeyEvent(key_event));
|
||||
match crossterm::event::poll(poll_timeout) {
|
||||
Ok(true) => match crossterm::event::read() {
|
||||
Ok(event) => {
|
||||
match event {
|
||||
crossterm::event::Event::Key(key_event) => {
|
||||
// Some Windows terminals (e.g., legacy conhost) only report
|
||||
// `Release` events when keyboard enhancement flags are not
|
||||
// supported. Preserve those events so onboarding works there.
|
||||
if !drop_release_events
|
||||
|| matches!(key_event.kind, KeyEventKind::Press | KeyEventKind::Repeat)
|
||||
{
|
||||
last_key_time = Instant::now();
|
||||
app_event_tx.send(AppEvent::KeyEvent(key_event));
|
||||
}
|
||||
}
|
||||
crossterm::event::Event::Resize(_, _) => {
|
||||
app_event_tx.send(AppEvent::RequestRedraw);
|
||||
}
|
||||
// When the terminal/tab regains focus, issue a redraw.
|
||||
// Some terminals clear the alt‑screen buffer on focus switches,
|
||||
// which can leave the status bar and inline images blank until
|
||||
// the next resize. A focus‑gain repaint fixes this immediately.
|
||||
crossterm::event::Event::FocusGained => {
|
||||
app_event_tx.send(AppEvent::RequestRedraw);
|
||||
}
|
||||
crossterm::event::Event::FocusLost => {
|
||||
// No action needed; keep state as‑is.
|
||||
}
|
||||
crossterm::event::Event::Paste(pasted) => {
|
||||
// Many terminals convert newlines to \r when pasting (e.g., iTerm2),
|
||||
// but tui-textarea expects \n. Normalize CR to LF.
|
||||
// [tui-textarea]: https://github.com/rhysd/tui-textarea/blob/4d18622eeac13b309e0ff6a55a46ac6706da68cf/src/textarea.rs#L782-L783
|
||||
// [iTerm2]: https://github.com/gnachman/iTerm2/blob/5d0c0d9f68523cbd0494dad5422998964a2ecd8d/sources/iTermPasteHelper.m#L206-L216
|
||||
let pasted = pasted.replace("\r", "\n");
|
||||
app_event_tx.send(AppEvent::Paste(pasted));
|
||||
}
|
||||
crossterm::event::Event::Mouse(mouse_event) => {
|
||||
app_event_tx.send(AppEvent::MouseEvent(mouse_event));
|
||||
}
|
||||
// All other event variants are explicitly handled above.
|
||||
}
|
||||
crossterm::event::Event::Resize(_, _) => {
|
||||
app_event_tx.send(AppEvent::RequestRedraw);
|
||||
}
|
||||
Err(err) => {
|
||||
if err.kind() == std::io::ErrorKind::Interrupted {
|
||||
continue;
|
||||
}
|
||||
// When the terminal/tab regains focus, issue a redraw.
|
||||
// Some terminals clear the alt‑screen buffer on focus switches,
|
||||
// which can leave the status bar and inline images blank until
|
||||
// the next resize. A focus‑gain repaint fixes this immediately.
|
||||
crossterm::event::Event::FocusGained => {
|
||||
app_event_tx.send(AppEvent::RequestRedraw);
|
||||
}
|
||||
crossterm::event::Event::FocusLost => {
|
||||
// No action needed; keep state as‑is.
|
||||
}
|
||||
crossterm::event::Event::Paste(pasted) => {
|
||||
// Many terminals convert newlines to \r when pasting (e.g., iTerm2),
|
||||
// but tui-textarea expects \n. Normalize CR to LF.
|
||||
// [tui-textarea]: https://github.com/rhysd/tui-textarea/blob/4d18622eeac13b309e0ff6a55a46ac6706da68cf/src/textarea.rs#L782-L783
|
||||
// [iTerm2]: https://github.com/gnachman/iTerm2/blob/5d0c0d9f68523cbd0494dad5422998964a2ecd8d/sources/iTermPasteHelper.m#L206-L216
|
||||
let pasted = pasted.replace("\r", "\n");
|
||||
app_event_tx.send(AppEvent::Paste(pasted));
|
||||
}
|
||||
crossterm::event::Event::Mouse(mouse_event) => {
|
||||
app_event_tx.send(AppEvent::MouseEvent(mouse_event));
|
||||
}
|
||||
// All other event variants are explicitly handled above.
|
||||
tracing::error!("input thread failed to read event: {err}");
|
||||
input_running_thread.store(false, Ordering::Release);
|
||||
app_event_tx.send(AppEvent::ExitRequest);
|
||||
break;
|
||||
}
|
||||
},
|
||||
Ok(false) => {
|
||||
// Timeout expired, no `Event` is available. If the user is typing
|
||||
// keep the loop hot; otherwise sleep briefly to cut idle CPU.
|
||||
if !hot_typing {
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Timeout expired, no `Event` is available. If the user is typing
|
||||
// keep the loop hot; otherwise sleep briefly to cut idle CPU.
|
||||
if !hot_typing {
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
Err(err) => {
|
||||
if err.kind() == std::io::ErrorKind::Interrupted {
|
||||
continue;
|
||||
}
|
||||
tracing::error!("input thread failed to poll events: {err}");
|
||||
input_running_thread.store(false, Ordering::Release);
|
||||
app_event_tx.send(AppEvent::ExitRequest);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ pub(crate) struct ChatComposer {
|
||||
// Tracks a one-off Tab-triggered file search. When set, we will only
|
||||
// create/show a popup if the results are non-empty to avoid flicker.
|
||||
pending_tab_file_query: Option<String>,
|
||||
file_popup_origin: Option<FilePopupOrigin>,
|
||||
pending_pastes: Vec<(String, String)>,
|
||||
token_usage_info: Option<TokenUsageInfo>,
|
||||
has_focus: bool,
|
||||
@@ -145,6 +146,11 @@ enum ActivePopup {
|
||||
File(FileSearchPopup),
|
||||
}
|
||||
|
||||
enum FilePopupOrigin {
|
||||
Auto,
|
||||
Manual { token: String },
|
||||
}
|
||||
|
||||
impl ChatComposer {
|
||||
pub fn new(
|
||||
has_input_focus: bool,
|
||||
@@ -165,6 +171,7 @@ impl ChatComposer {
|
||||
dismissed_file_popup_token: None,
|
||||
current_file_query: None,
|
||||
pending_tab_file_query: None,
|
||||
file_popup_origin: None,
|
||||
pending_pastes: Vec::new(),
|
||||
token_usage_info: None,
|
||||
has_focus: has_input_focus,
|
||||
@@ -694,10 +701,42 @@ impl ChatComposer {
|
||||
pub(crate) fn on_file_search_result(&mut self, query: String, matches: Vec<FileMatch>) {
|
||||
// Handle one-off Tab-triggered case first: only open if matches exist.
|
||||
if self.pending_tab_file_query.as_ref() == Some(&query) {
|
||||
// If the user kept typing while the search was in-flight, resync to the
|
||||
// latest token before applying stale results.
|
||||
if let Some(current_token) = Self::current_generic_token(&self.textarea) {
|
||||
if Self::current_completion_token(&self.textarea).is_some() {
|
||||
// A new auto-triggerable token (e.g., @ or ./) should be handled by the
|
||||
// standard auto completion path instead of hijacking the manual popup.
|
||||
self.pending_tab_file_query = None;
|
||||
self.file_popup_origin = None;
|
||||
self.current_file_query = None;
|
||||
return;
|
||||
}
|
||||
if !current_token.is_empty() && current_token != query {
|
||||
self.pending_tab_file_query = Some(current_token.clone());
|
||||
if let ActivePopup::File(popup) = &mut self.active_popup {
|
||||
popup.set_query(¤t_token);
|
||||
} else {
|
||||
let mut popup = FileSearchPopup::new();
|
||||
popup.set_query(¤t_token);
|
||||
self.active_popup = ActivePopup::File(popup);
|
||||
}
|
||||
self.current_file_query = Some(current_token.clone());
|
||||
self.file_popup_origin = Some(FilePopupOrigin::Manual { token: current_token.clone() });
|
||||
self.app_event_tx
|
||||
.send(crate::app_event::AppEvent::StartFileSearch(current_token));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear pending regardless of result to avoid repeats.
|
||||
self.pending_tab_file_query = None;
|
||||
|
||||
if matches.is_empty() {
|
||||
if let ActivePopup::File(popup) = &mut self.active_popup {
|
||||
// Clear the waiting state so the popup shows "no matches" instead of spinning forever.
|
||||
popup.set_matches(&query, Vec::new());
|
||||
}
|
||||
return; // do not open popup when no matches to avoid flicker
|
||||
}
|
||||
|
||||
@@ -710,11 +749,21 @@ impl ChatComposer {
|
||||
self.active_popup = ActivePopup::File(popup);
|
||||
}
|
||||
}
|
||||
self.current_file_query = Some(query);
|
||||
self.current_file_query = Some(query.clone());
|
||||
self.file_popup_origin = Some(FilePopupOrigin::Manual { token: query });
|
||||
self.dismissed_file_popup_token = None;
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(self.file_popup_origin, Some(FilePopupOrigin::Manual { .. }))
|
||||
&& self.current_file_query.as_ref() == Some(&query)
|
||||
{
|
||||
if let ActivePopup::File(popup) = &mut self.active_popup {
|
||||
popup.set_matches(&query, matches);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, only apply if user is still editing a token matching the query
|
||||
// and that token qualifies for auto-trigger (i.e., @ or ./).
|
||||
let current_opt = Self::current_completion_token(&self.textarea);
|
||||
@@ -751,6 +800,8 @@ impl ChatComposer {
|
||||
match self.active_popup {
|
||||
ActivePopup::File(_) => {
|
||||
self.active_popup = ActivePopup::None;
|
||||
self.file_popup_origin = None;
|
||||
self.current_file_query = None;
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
@@ -991,6 +1042,8 @@ impl ChatComposer {
|
||||
self.dismissed_file_popup_token = Some(tok.to_string());
|
||||
}
|
||||
self.active_popup = ActivePopup::None;
|
||||
self.file_popup_origin = None;
|
||||
self.current_file_query = None;
|
||||
(InputResult::None, true)
|
||||
}
|
||||
KeyEvent {
|
||||
@@ -1006,6 +1059,8 @@ impl ChatComposer {
|
||||
// Drop popup borrow before using self mutably again.
|
||||
self.insert_selected_path(&sel_path);
|
||||
self.active_popup = ActivePopup::None;
|
||||
self.file_popup_origin = None;
|
||||
self.current_file_query = None;
|
||||
return (InputResult::None, true);
|
||||
}
|
||||
(InputResult::None, false)
|
||||
@@ -1556,48 +1611,77 @@ impl ChatComposer {
|
||||
/// Note this is only called when self.active_popup is NOT Command.
|
||||
fn sync_file_search_popup(&mut self) {
|
||||
// Determine if there is a token underneath the cursor worth completing.
|
||||
let query = match Self::current_completion_token(&self.textarea) {
|
||||
Some(token) => token,
|
||||
match Self::current_completion_token(&self.textarea) {
|
||||
Some(query) => {
|
||||
if self.dismissed_file_popup_token.as_ref() == Some(&query) {
|
||||
return;
|
||||
}
|
||||
|
||||
if query.chars().count() >= 1 {
|
||||
self.app_event_tx
|
||||
.send(crate::app_event::AppEvent::StartFileSearch(query.clone()));
|
||||
}
|
||||
|
||||
match &mut self.active_popup {
|
||||
ActivePopup::File(popup) => {
|
||||
if query.is_empty() {
|
||||
popup.set_empty_prompt();
|
||||
} else {
|
||||
popup.set_query(&query);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let mut popup = FileSearchPopup::new();
|
||||
if query.is_empty() {
|
||||
popup.set_empty_prompt();
|
||||
} else {
|
||||
popup.set_query(&query);
|
||||
}
|
||||
self.active_popup = ActivePopup::File(popup);
|
||||
}
|
||||
}
|
||||
|
||||
self.current_file_query = Some(query);
|
||||
self.file_popup_origin = Some(FilePopupOrigin::Auto);
|
||||
self.dismissed_file_popup_token = None;
|
||||
}
|
||||
None => {
|
||||
// Allow manually-triggered popups (via Tab) to stay open while the
|
||||
// cursor remains within the same generic token. When the token
|
||||
// changes, trigger a new search; otherwise keep the popup stable.
|
||||
if let Some(FilePopupOrigin::Manual { token }) = &mut self.file_popup_origin {
|
||||
if let Some(generic) = Self::current_generic_token(&self.textarea) {
|
||||
if generic.is_empty() {
|
||||
self.active_popup = ActivePopup::None;
|
||||
self.dismissed_file_popup_token = None;
|
||||
self.file_popup_origin = None;
|
||||
self.current_file_query = None;
|
||||
} else {
|
||||
if *token != generic {
|
||||
*token = generic.clone();
|
||||
if let ActivePopup::File(popup) = &mut self.active_popup {
|
||||
popup.set_query(&generic);
|
||||
} else {
|
||||
let mut popup = FileSearchPopup::new();
|
||||
popup.set_query(&generic);
|
||||
self.active_popup = ActivePopup::File(popup);
|
||||
}
|
||||
self.current_file_query = Some(generic.clone());
|
||||
self.app_event_tx
|
||||
.send(crate::app_event::AppEvent::StartFileSearch(generic));
|
||||
}
|
||||
// Keep popup visible while token is still active.
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.active_popup = ActivePopup::None;
|
||||
self.dismissed_file_popup_token = None;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// If user dismissed popup for this exact query, don't reopen until text changes.
|
||||
if self.dismissed_file_popup_token.as_ref() == Some(&query) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Trigger file search as soon as at least 1 character is typed.
|
||||
// The popup shows an idle hint for an empty query handled above.
|
||||
if query.chars().count() >= 1 {
|
||||
self.app_event_tx
|
||||
.send(crate::app_event::AppEvent::StartFileSearch(query.clone()));
|
||||
}
|
||||
|
||||
match &mut self.active_popup {
|
||||
ActivePopup::File(popup) => {
|
||||
if query.is_empty() {
|
||||
popup.set_empty_prompt();
|
||||
} else {
|
||||
popup.set_query(&query);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let mut popup = FileSearchPopup::new();
|
||||
if query.is_empty() {
|
||||
popup.set_empty_prompt();
|
||||
} else {
|
||||
popup.set_query(&query);
|
||||
}
|
||||
self.active_popup = ActivePopup::File(popup);
|
||||
self.file_popup_origin = None;
|
||||
self.current_file_query = None;
|
||||
}
|
||||
}
|
||||
|
||||
self.current_file_query = Some(query);
|
||||
self.dismissed_file_popup_token = None;
|
||||
}
|
||||
|
||||
pub(crate) fn set_has_focus(&mut self, has_focus: bool) {
|
||||
|
||||
@@ -322,6 +322,13 @@ fn limits_overlay_renders_snapshot() {
|
||||
.iter()
|
||||
.position(|line| line.contains("7 Day History"))
|
||||
.expect("expected usage header");
|
||||
assert!(
|
||||
strings
|
||||
.get(header_idx.saturating_sub(1))
|
||||
.map(|line| line.trim().is_empty())
|
||||
.unwrap_or(false),
|
||||
"expected blank spacer before 7 Day History"
|
||||
);
|
||||
let latest_label = Local::now().format("%b %d").to_string();
|
||||
let yesterday_label = (Local::now().date_naive() - ChronoDuration::days(1))
|
||||
.format("%b %d")
|
||||
@@ -343,6 +350,13 @@ fn limits_overlay_renders_snapshot() {
|
||||
.iter()
|
||||
.position(|line| line.contains("6 Month History"))
|
||||
.expect("expected monthly usage header");
|
||||
assert!(
|
||||
strings
|
||||
.get(month_header_idx.saturating_sub(1))
|
||||
.map(|line| line.trim().is_empty())
|
||||
.unwrap_or(false),
|
||||
"expected blank spacer before 6 Month History"
|
||||
);
|
||||
let current_month_label = Local::now().format("%b %Y").to_string();
|
||||
let month_line = strings
|
||||
.get(month_header_idx + 1)
|
||||
|
||||
+19
-7
@@ -403,12 +403,21 @@ fn print_timing_summary(summary: &str) {
|
||||
|
||||
#[allow(clippy::print_stdout, clippy::print_stderr)]
|
||||
fn cleanup_session_worktrees_and_print() {
|
||||
use std::process::Command;
|
||||
let pid = std::process::id();
|
||||
let home = match std::env::var_os("HOME") { Some(h) => std::path::PathBuf::from(h), None => return };
|
||||
let session_dir = home.join(".code").join("working").join("_session");
|
||||
let file = session_dir.join(format!("pid-{}.txt", pid));
|
||||
let Ok(data) = std::fs::read_to_string(&file) else { return };
|
||||
reclaim_worktrees_from_file(&file, "current session");
|
||||
}
|
||||
|
||||
fn reclaim_worktrees_from_file(path: &std::path::Path, label: &str) {
|
||||
use std::process::Command;
|
||||
|
||||
let Ok(data) = std::fs::read_to_string(path) else {
|
||||
let _ = std::fs::remove_file(path);
|
||||
return;
|
||||
};
|
||||
|
||||
let mut entries: Vec<(std::path::PathBuf, std::path::PathBuf)> = Vec::new();
|
||||
for line in data.lines() {
|
||||
if line.trim().is_empty() { continue; }
|
||||
@@ -416,22 +425,25 @@ fn cleanup_session_worktrees_and_print() {
|
||||
entries.push((std::path::PathBuf::from(root_s), std::path::PathBuf::from(path_s)));
|
||||
}
|
||||
}
|
||||
// Deduplicate paths in case of retries
|
||||
|
||||
use std::collections::HashSet;
|
||||
let mut seen = HashSet::new();
|
||||
entries.retain(|(_, p)| seen.insert(p.clone()));
|
||||
if entries.is_empty() { let _ = std::fs::remove_file(&file); return; }
|
||||
if entries.is_empty() {
|
||||
let _ = std::fs::remove_file(path);
|
||||
return;
|
||||
}
|
||||
|
||||
eprintln!("Cleaning remaining worktrees ({}).", entries.len());
|
||||
eprintln!("Cleaning remaining worktrees for {} ({}).", label, entries.len());
|
||||
for (git_root, worktree) in entries {
|
||||
let wt_str = match worktree.to_str() { Some(s) => s, None => continue };
|
||||
let Some(wt_str) = worktree.to_str() else { continue };
|
||||
let _ = Command::new("git")
|
||||
.current_dir(&git_root)
|
||||
.args(["worktree", "remove", wt_str, "--force"])
|
||||
.output();
|
||||
let _ = std::fs::remove_dir_all(&worktree);
|
||||
}
|
||||
let _ = std::fs::remove_file(&file);
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
fn maybe_apply_terminal_theme_detection(config: &mut Config, theme_configured_explicitly: bool) {
|
||||
|
||||
Reference in New Issue
Block a user