merge code-branch-20250929-011511 via /merge

This commit is contained in:
James Peter
2025-09-29 12:11:43 +10:00
13 changed files with 884 additions and 84 deletions
+12
View File
@@ -3,6 +3,18 @@
> [!TIP]
> We're constantly improving Code! This page documents the core changes. You can also check our [releases page](https://github.com/just-every/code/releases) for additional information.
## [0.2.173] - 2025-09-29
- TUI/Browser: auto hand off /browser startup failures to Code so sessions self-heal. (95e27cd0)
- TUI/Browser: sanitize and surface error details when handoff triggers for faster diagnosis. (95e27cd0)
## [0.2.172] - 2025-09-28
- CLI: introduce a responses API proxy command so shared hosts can forward Responses calls securely. (c5494815)
- MCP: add streamable HTTP client support and tighten per-call timeout handling. (3a1be084, bd6aad0b, d3ecf4de, 9bb84892)
- Auto-drive: stream coordinator reasoning, keep plan context, and smooth heading presentation. (fc8da8cf, 0d5bc1f1, 45a6fb94, aaa91865)
- TUI/History: route diff and explore cells through domain events for consistent playback. (0ae94dca, 0d5d57ee)
## [0.2.170] - 2025-09-27
- TUI/History: drive exec, assistant, explore, and rate-limit cells from domain events for consistent streaming. (38abccc, 5409493, e6e72d3, f851369, d7e9ee7)
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@just-every/code",
"version": "0.2.171",
"version": "0.2.173",
"license": "Apache-2.0",
"description": "Lightweight coding agent that runs in your terminal - fork of OpenAI Codex",
"bin": {
@@ -35,10 +35,10 @@
"prettier": "^3.3.3"
},
"optionalDependencies": {
"@just-every/code-darwin-arm64": "0.2.171",
"@just-every/code-darwin-x64": "0.2.171",
"@just-every/code-linux-x64-musl": "0.2.171",
"@just-every/code-linux-arm64-musl": "0.2.171",
"@just-every/code-win32-x64": "0.2.171"
"@just-every/code-darwin-arm64": "0.2.173",
"@just-every/code-darwin-x64": "0.2.173",
"@just-every/code-linux-x64-musl": "0.2.173",
"@just-every/code-linux-arm64-musl": "0.2.173",
"@just-every/code-win32-x64": "0.2.173"
}
}
+1 -1
View File
@@ -1803,7 +1803,7 @@ impl App<'_> {
}
SlashCommand::Update => {
if let AppState::Chat { widget } = &mut self.app_state {
widget.handle_update_command();
widget.handle_update_command(command_args.trim());
}
}
SlashCommand::Notifications => {
@@ -1,7 +1,8 @@
use crate::app_event::AppEvent;
use crate::app_event_sender::AppEventSender;
use crate::colors;
use crate::auto_drive_strings;
use crate::colors;
use crate::header_wave::{HeaderBorderWeaveEffect, HeaderWaveEffect};
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
@@ -10,7 +11,7 @@ use ratatui::widgets::{Block, Borders, Paragraph, Widget, Wrap};
use unicode_width::UnicodeWidthStr;
use std::borrow::Cow;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use std::time::{Duration, Instant};
const DRIVE_SPINNER_FRAMES: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
const DRIVE_SPINNER_INTERVAL_MS: u64 = 120;
@@ -43,6 +44,7 @@ pub(crate) struct AutoCoordinatorViewModel {
pub button: Option<AutoCoordinatorButton>,
pub manual_hint: Option<String>,
pub ctrl_switch_hint: String,
pub cli_running: bool,
}
struct VariantContext {
@@ -54,11 +56,29 @@ struct VariantContext {
pub(crate) struct AutoCoordinatorView {
model: AutoCoordinatorViewModel,
app_event_tx: AppEventSender,
header_wave: HeaderWaveEffect,
header_border: HeaderBorderWeaveEffect,
}
impl AutoCoordinatorView {
pub fn new(model: AutoCoordinatorViewModel, app_event_tx: AppEventSender) -> Self {
Self { model, app_event_tx }
let now = Instant::now();
let header_wave = {
let effect = HeaderWaveEffect::new();
effect.set_enabled(false, now);
effect
};
let header_border = {
let effect = HeaderBorderWeaveEffect::new();
effect.set_enabled(false, now);
effect
};
Self {
model,
app_event_tx,
header_wave,
header_border,
}
}
fn build_context(&self) -> VariantContext {
@@ -74,7 +94,7 @@ impl AutoCoordinatorView {
}
}
fn render_frame(&self, area: Rect, buf: &mut Buffer, title: &str) -> Option<Rect> {
fn render_frame(&self, area: Rect, buf: &mut Buffer, title: &str, now: Instant) -> Option<Rect> {
if area.width < 3 || area.height < 3 {
return None;
}
@@ -89,6 +109,28 @@ impl AutoCoordinatorView {
.borders(Borders::ALL)
.border_style(Style::default().fg(colors::border()))
.render(area, buf);
if self.header_wave.is_enabled() {
self.header_wave.render(area, buf, now);
}
if self.header_border.is_enabled() {
self.header_border.render(area, buf, now);
}
// Reapply static title styling so animation never recolors it
let title_style = Style::default()
.fg(colors::text())
.add_modifier(Modifier::BOLD);
let title_y = area.y;
let title_start = area.x + 1;
for (offset, ch) in title.chars().enumerate() {
let x = title_start + offset as u16;
if x >= area.x && x < area.x.saturating_add(area.width) {
let mut ch_buf = [0u8; 4];
let symbol = ch.encode_utf8(&mut ch_buf);
let cell = &mut buf[(x, title_y)];
cell.set_symbol(symbol);
cell.set_style(title_style);
}
}
Some(Rect {
x: area.x + 1,
y: area.y + 1,
@@ -290,7 +332,25 @@ impl AutoCoordinatorView {
}
fn render_classic(&self, area: Rect, buf: &mut Buffer, ctx: &VariantContext) {
let Some(inner) = self.render_frame(area, buf, " Auto Drive ") else {
let now = Instant::now();
let waiting = self.model.waiting_for_response;
let mut frame_needed = false;
let should_enable_border = waiting && !self.model.cli_running;
let mut border_enabled = self.header_border.is_enabled();
if should_enable_border && !border_enabled {
self.header_border.set_enabled(true, now);
border_enabled = true;
} else if !should_enable_border && border_enabled {
self.header_border.set_enabled(false, now);
border_enabled = false;
}
if border_enabled && self.header_border.schedule_if_needed(now) {
frame_needed = true;
}
let Some(inner) = self.render_frame(area, buf, " Auto Drive ", now) else {
return;
};
let inner = self.apply_left_padding(inner, buf);
@@ -349,8 +409,12 @@ impl AutoCoordinatorView {
.wrap(Wrap { trim: true })
.render(inner, buf);
let mut next_interval = Duration::from_millis(DRIVE_SPINNER_INTERVAL_MS);
if frame_needed {
next_interval = next_interval.min(HeaderBorderWeaveEffect::FRAME_INTERVAL);
}
self.app_event_tx
.send(AppEvent::ScheduleFrameIn(Duration::from_millis(DRIVE_SPINNER_INTERVAL_MS)));
.send(AppEvent::ScheduleFrameIn(next_interval));
}
fn apply_left_padding(&self, area: Rect, buf: &mut Buffer) -> Rect {
@@ -100,7 +100,7 @@ impl UpdateSettingsView {
.unwrap_or_else(|| command.join(" "));
self.app_event_tx.send_background_event(format!(
"⬆️ Update available: {} → {}. Running `{}`...",
"⬆️ Update available: {} → {}. Opening guided upgrade with `{}`…",
self.current_version, latest, display
));
self.app_event_tx.send(AppEvent::RunUpdateCommand {
@@ -109,7 +109,7 @@ impl UpdateSettingsView {
latest_version: Some(latest.clone()),
});
self.app_event_tx.send_background_event(format!(
"↻ Restart Code after `{}` completes to finish upgrading to {}.",
"↻ Complete the guided terminal steps for `{}` then restart Code to finish upgrading to {}.",
display, latest
));
self.is_complete = true;
+264 -27
View File
@@ -16,7 +16,7 @@ use std::time::{Duration, Instant, SystemTime};
use ratatui::style::Modifier;
use ratatui::style::Style;
use crate::header_wave::HeaderWaveEffect;
use crate::header_wave::{HeaderBorderWeaveEffect, HeaderWaveEffect};
use crate::auto_drive_strings;
use codex_common::elapsed::format_duration;
@@ -70,6 +70,7 @@ use self::agent_install::{
start_agent_install_session,
start_direct_terminal_session,
start_prompt_terminal_session,
start_upgrade_terminal_session,
wrap_command,
};
use self::auto_coordinator::{start_auto_coordinator, AutoCoordinatorCommand};
@@ -117,6 +118,7 @@ use crate::bottom_pane::{
AutoCoordinatorViewModel,
CountdownState,
};
use crate::exec_command::strip_bash_lc_and_escape;
use codex_git_tooling::{
create_ghost_commit,
restore_ghost_commit,
@@ -166,6 +168,7 @@ const STATUS_LABEL_GAP: usize = 2;
const STATUS_CONTENT_PREFIX: &str = " ";
const STATUS_TOKENS_PREFIX: &str = " ";
const AUTO_COUNTDOWN_SECONDS: u8 = 10;
const ENABLE_WARP_STRIPES: bool = false;
fn status_field_prefix(label: &str) -> String {
let padding = STATUS_LABEL_GAP
@@ -522,12 +525,14 @@ pub(crate) struct ChatWidget<'a> {
tools_state: ToolState,
live_builder: RowBuilder,
header_wave: HeaderWaveEffect,
header_border: HeaderBorderWeaveEffect,
// Store pending image paths keyed by their placeholder text
pending_images: HashMap<String, PathBuf>,
// (removed) pending non-image files are no longer tracked; non-image paths remain as plain text
welcome_shown: bool,
// Path to the latest browser screenshot and URL for display
latest_browser_screenshot: Arc<Mutex<Option<(PathBuf, String)>>>,
browser_autofix_requested: Arc<AtomicBool>,
// Cached image protocol to avoid recreating every frame (path, area, protocol)
cached_image_protocol:
std::cell::RefCell<Option<(PathBuf, Rect, ratatui_image::protocol::Protocol)>>,
@@ -2766,12 +2771,22 @@ impl ChatWidget<'_> {
live_builder: RowBuilder::new(usize::MAX),
header_wave: {
let effect = HeaderWaveEffect::new();
if ENABLE_WARP_STRIPES {
effect.set_enabled(true, Instant::now());
} else {
effect.set_enabled(false, Instant::now());
}
effect
},
header_border: {
let effect = HeaderBorderWeaveEffect::new();
effect.set_enabled(false, Instant::now());
effect
},
pending_images: HashMap::new(),
welcome_shown: false,
latest_browser_screenshot: Arc::new(Mutex::new(None)),
browser_autofix_requested: Arc::new(AtomicBool::new(false)),
cached_image_protocol: RefCell::new(None),
cached_picker: RefCell::new(terminal_info.picker.clone()),
cached_cell_size: std::cell::OnceCell::new(),
@@ -3010,12 +3025,22 @@ impl ChatWidget<'_> {
live_builder: RowBuilder::new(usize::MAX),
header_wave: {
let effect = HeaderWaveEffect::new();
if ENABLE_WARP_STRIPES {
effect.set_enabled(true, Instant::now());
} else {
effect.set_enabled(false, Instant::now());
}
effect
},
header_border: {
let effect = HeaderBorderWeaveEffect::new();
effect.set_enabled(false, Instant::now());
effect
},
pending_images: HashMap::new(),
welcome_shown: false,
latest_browser_screenshot: Arc::new(Mutex::new(None)),
browser_autofix_requested: Arc::new(AtomicBool::new(false)),
cached_image_protocol: RefCell::new(None),
cached_picker: RefCell::new(terminal_info.picker.clone()),
cached_cell_size: std::cell::OnceCell::new(),
@@ -8608,16 +8633,48 @@ impl ChatWidget<'_> {
}
}
pub(crate) fn handle_update_command(&mut self) {
if crate::updates::upgrade_ui_enabled() {
pub(crate) fn handle_update_command(&mut self, command_args: &str) {
if !crate::updates::upgrade_ui_enabled() {
self.app_event_tx.send_background_event(
"`/update` — updates are disabled in debug builds. Set SHOW_UPGRADE=1 to preview.".
to_string(),
);
return;
}
let trimmed = command_args.trim();
if trimmed.eq_ignore_ascii_case("settings")
|| trimmed.eq_ignore_ascii_case("ui")
|| trimmed.eq_ignore_ascii_case("config")
{
self.show_update_settings_ui();
return;
}
self.app_event_tx.send_background_event(
"`/update` — updates are disabled in debug builds. Set SHOW_UPGRADE=1 to preview.".
to_string(),
);
match crate::updates::resolve_upgrade_resolution() {
crate::updates::UpgradeResolution::Command { command, display } => {
if command.is_empty() {
self.history_push(history_cell::new_error_event(
"`/upgrade` — no upgrade command available for this install.".to_string(),
));
self.request_redraw();
return;
}
let latest = self.latest_upgrade_version.clone();
self.push_background_tail(
"Opening a guided upgrade terminal to finish installing updates.".to_string(),
);
if let Some(launch) = self.launch_update_command(command, display, latest) {
self.app_event_tx.send(AppEvent::OpenTerminal(launch));
}
}
crate::updates::UpgradeResolution::Manual { instructions } => {
self.show_update_settings_ui();
self.history_push(history_cell::new_background_event(instructions));
self.request_redraw();
}
}
}
pub(crate) fn handle_notifications_command(&mut self, args: String) {
@@ -9564,6 +9621,44 @@ impl ChatWidget<'_> {
);
}
fn is_cli_running(&self) -> bool {
if !self.exec.running_commands.is_empty() {
return true;
}
if !self.tools_state.running_custom_tools.is_empty()
|| !self.tools_state.running_web_search.is_empty()
|| !self.tools_state.running_wait_tools.is_empty()
|| !self.tools_state.running_kill_tools.is_empty()
{
return true;
}
if self.stream.is_write_cycle_active() {
return true;
}
if !self.active_task_ids.is_empty() {
return true;
}
false
}
fn update_header_border_activation(&self) {
let now = Instant::now();
let should_enable_header = self.auto_state.active && self.is_cli_running();
let currently_enabled = self.header_border.is_enabled();
if should_enable_header && !currently_enabled {
self.header_border.set_enabled(true, now);
} else if !should_enable_header && currently_enabled {
self.header_border.set_enabled(false, now);
}
}
fn refresh_auto_drive_visuals(&mut self) {
self.update_header_border_activation();
if self.auto_state.active {
self.auto_rebuild_live_ring();
}
}
pub(crate) fn handle_auto_command(&mut self, goal: Option<String>) {
let provided = goal.unwrap_or_default();
let trimmed = provided.trim();
@@ -9592,10 +9687,10 @@ impl ChatWidget<'_> {
self.bottom_pane.set_task_running(true);
self.bottom_pane
.update_status_text("Auto Drive Goal".to_string());
self.header_wave.set_enabled(false, Instant::now());
self.push_background_tail(
"Please enter the goal you would like to work autonomously towards.".to_string(),
);
self.update_header_border_activation();
self.request_redraw();
return;
}
@@ -9629,7 +9724,7 @@ impl ChatWidget<'_> {
self.auto_state.last_broadcast_thought = None;
self.auto_state.seconds_remaining = AUTO_COUNTDOWN_SECONDS;
self.auto_state.waiting_for_response = true;
self.header_wave.set_enabled(true, Instant::now());
self.update_header_border_activation();
self.auto_rebuild_live_ring();
self.push_background_tail(format!("Auto Drive started: {goal_text}"));
self.request_redraw();
@@ -9663,6 +9758,7 @@ impl ChatWidget<'_> {
self.auto_state.placeholder_phrase =
Some(auto_drive_strings::next_auto_drive_phrase().to_string());
self.auto_state.thinking_prefix_stripped = false;
self.update_header_border_activation();
self.auto_rebuild_live_ring();
self.request_redraw();
}
@@ -9684,6 +9780,7 @@ impl ChatWidget<'_> {
self.auto_state.resume_after_manual_submit = false;
self.auto_state.awaiting_submission = false;
self.auto_state.waiting_for_response = false;
self.update_header_border_activation();
match status {
AutoCoordinatorStatus::Continue => {
@@ -9876,6 +9973,7 @@ impl ChatWidget<'_> {
self.bottom_pane.set_task_running(true);
self.bottom_pane
.update_status_text("Auto Drive".to_string());
self.update_header_border_activation();
self.auto_rebuild_live_ring();
self.request_redraw();
}
@@ -9906,7 +10004,10 @@ impl ChatWidget<'_> {
self.bottom_pane.update_status_text(String::new());
}
self.bottom_pane.ensure_input_focus();
self.header_wave.set_enabled(false, Instant::now());
if ENABLE_WARP_STRIPES {
self.header_wave.set_enabled(false, Instant::now());
}
self.update_header_border_activation();
self.request_redraw();
}
@@ -9925,6 +10026,7 @@ impl ChatWidget<'_> {
self.auto_state.placeholder_phrase = None;
self.auto_state.thinking_prefix_stripped = false;
self.auto_state.last_decision_display = None;
self.update_header_border_activation();
self.auto_rebuild_live_ring();
self.request_redraw();
self.auto_send_conversation();
@@ -9982,6 +10084,7 @@ impl ChatWidget<'_> {
}
status_lines.push(telemetry_line);
}
let cli_running = self.is_cli_running();
let prompt = self
.auto_state
@@ -10036,6 +10139,7 @@ impl ChatWidget<'_> {
button,
manual_hint,
ctrl_switch_hint,
cli_running,
};
self.bottom_pane
@@ -10220,19 +10324,52 @@ impl ChatWidget<'_> {
return None;
}
let id = self.terminal.alloc_id();
if let Some(version) = latest_version {
self.pending_upgrade_notice = Some((id, version));
let command_text = if display.trim().is_empty() {
strip_bash_lc_and_escape(&command)
} else {
display.clone()
};
if command_text.trim().is_empty() {
self.history_push(history_cell::new_error_event(
"`/update` — unable to resolve upgrade command text.".to_string(),
));
self.request_redraw();
return None;
}
Some(TerminalLaunch {
let id = self.terminal.alloc_id();
if let Some(version) = &latest_version {
self.pending_upgrade_notice = Some((id, version.clone()));
}
let (controller_tx, controller_rx) = mpsc::channel();
let controller = TerminalRunController { tx: controller_tx };
let display_label = Self::truncate_with_ellipsis(&format!("Guided: {command_text}"), 128);
let launch = TerminalLaunch {
id,
title: "Upgrade Code".to_string(),
command,
command_display: display,
controller: None,
command: Vec::new(),
command_display: display_label,
controller: Some(controller.clone()),
auto_close_on_success: false,
start_running: true,
})
};
let cwd = self.config.cwd.to_string_lossy().to_string();
start_upgrade_terminal_session(
self.app_event_tx.clone(),
id,
command_text,
latest_version,
Some(cwd),
controller,
controller_rx,
self.config.debug,
);
Some(launch)
}
pub(crate) fn terminal_open(&mut self, launch: &TerminalLaunch) {
@@ -11811,10 +11948,17 @@ impl ChatWidget<'_> {
let tx = self.app_event_tx.clone();
tokio::spawn(async move {
match crate::updates::auto_upgrade_if_enabled(&cfg).await {
Ok(Some(version)) => {
tx.send(AppEvent::AutoUpgradeCompleted { version });
Ok(outcome) => {
if let Some(version) = outcome.installed_version {
tx.send(AppEvent::AutoUpgradeCompleted { version });
}
if let Some(message) = outcome.user_notice {
tx.send(AppEvent::InsertBackgroundEvent {
message,
placement: BackgroundPlacement::Tail,
});
}
}
Ok(None) => {}
Err(err) => {
tracing::warn!("auto-upgrade: background task failed: {err:?}");
}
@@ -14000,6 +14144,56 @@ impl ChatWidget<'_> {
));
}
fn schedule_browser_autofix(
app_event_tx: AppEventSender,
autofix_state: Arc<AtomicBool>,
failure_context: &str,
raw_error: String,
) {
if autofix_state
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_err()
{
tracing::info!(
"[/browser] auto-handoff already requested; skipping duplicate dispatch"
);
return;
}
let sanitized = raw_error.replace('\n', " ").replace('\r', " ");
let trimmed = sanitized.trim();
let truncated = if trimmed.len() > 220 {
let mut shortened = trimmed.chars().take(220).collect::<String>();
shortened.push('…');
shortened
} else {
trimmed.to_string()
};
tracing::info!(
"[/browser] scheduling Code autofix for context='{}', error='{}'",
failure_context,
truncated
);
let visible_message = format!(
"🤖 Handing /browser failure ({}) to Code. Error: {}",
failure_context,
truncated
);
app_event_tx.send_background_event(visible_message);
let command_text = format!(
"/code The /browser command failed to {context}. Recent error: {error}. Please diagnose and fix the environment (for example, install or configure Chrome) so /browser works in this workspace.",
context = failure_context,
error = truncated
);
app_event_tx.send(AppEvent::DispatchCommand(
SlashCommand::Code,
command_text,
));
}
pub(crate) fn handle_browser_command(&mut self, command_text: String) {
// Parse the browser subcommand
let trimmed = command_text.trim();
@@ -14015,6 +14209,7 @@ impl ChatWidget<'_> {
// Toggle asynchronously: if internal browser is active, disable it; otherwise enable and open about:blank
let app_event_tx = self.app_event_tx.clone();
let browser_autofix_flag = self.browser_autofix_requested.clone();
tokio::spawn(async move {
let browser_manager = ChatWidget::get_browser_manager().await;
// Determine if internal browser is currently active
@@ -14047,14 +14242,26 @@ impl ChatWidget<'_> {
}
if let Err(e) = browser_manager.start().await {
tracing::error!("[/browser] failed to start internal browser: {}", e);
let error_text = e.to_string();
tracing::error!(
"[/browser] failed to start internal browser: {}",
error_text
);
app_event_tx.send_background_event(format!(
"❌ Failed to start internal browser: {}",
e
error_text
));
ChatWidget::schedule_browser_autofix(
app_event_tx.clone(),
browser_autofix_flag.clone(),
"start the internal browser",
error_text,
);
return;
}
browser_autofix_flag.store(false, Ordering::SeqCst);
// Set as global manager so core/session share the same instance
codex_browser::global::set_global_browser_manager(browser_manager.clone())
.await;
@@ -14096,6 +14303,7 @@ impl ChatWidget<'_> {
// Navigate to URL and wait for it to load
let latest_screenshot = self.latest_browser_screenshot.clone();
let app_event_tx = self.app_event_tx.clone();
let browser_autofix_flag = self.browser_autofix_requested.clone();
let url_for_goto = full_url.clone();
// Add status message
@@ -14124,10 +14332,26 @@ impl ChatWidget<'_> {
// IMPORTANT: Start the browser manager first before navigating
if let Err(e) = browser_manager.start().await {
tracing::error!("Failed to start TUI browser manager: {}", e);
let error_text = e.to_string();
tracing::error!(
"Failed to start TUI browser manager: {}",
error_text
);
app_event_tx.send_background_event(format!(
"❌ Failed to start internal browser: {}",
error_text
));
ChatWidget::schedule_browser_autofix(
app_event_tx.clone(),
browser_autofix_flag.clone(),
"launch the internal browser",
error_text,
);
return;
}
browser_autofix_flag.store(false, Ordering::SeqCst);
// Set up navigation callback to auto-capture screenshots
{
let latest_screenshot_callback = latest_screenshot.clone();
@@ -15856,20 +16080,33 @@ impl ChatWidget<'_> {
let status_line = Line::from(status_spans);
let now = Instant::now();
if self.header_wave.schedule_if_needed(now) {
self.update_header_border_activation();
let mut frame_needed = false;
if ENABLE_WARP_STRIPES && self.header_wave.schedule_if_needed(now) {
frame_needed = true;
}
if self.header_border.schedule_if_needed(now) {
frame_needed = true;
}
if frame_needed {
self.app_event_tx
.send(AppEvent::ScheduleFrameIn(HeaderWaveEffect::FRAME_INTERVAL));
.send(AppEvent::ScheduleFrameIn(HeaderBorderWeaveEffect::FRAME_INTERVAL));
}
// Render the block first
status_block.render(padded_area, buf);
let wave_enabled = self.header_wave.is_enabled();
let border_enabled = self.header_border.is_enabled();
if wave_enabled {
self.header_wave.render(padded_area, buf, now);
}
if border_enabled {
self.header_border.render(padded_area, buf, now);
}
// Then render the text inside with padding, centered
let status_style = if wave_enabled {
let effect_enabled = wave_enabled || border_enabled;
let status_style = if effect_enabled {
Style::default().fg(crate::colors::text())
} else {
Style::default()
+130 -8
View File
@@ -36,6 +36,10 @@ enum GuidedTerminalMode {
},
Prompt { user_prompt: String },
DirectCommand { command: String },
Upgrade {
initial_command: String,
latest_version: Option<String>,
},
}
#[derive(Debug, Deserialize)]
@@ -112,6 +116,45 @@ pub(super) fn start_direct_terminal_session(
);
}
pub(super) fn start_upgrade_terminal_session(
app_event_tx: AppEventSender,
terminal_id: u64,
initial_command: String,
latest_version: Option<String>,
cwd: Option<String>,
controller: TerminalRunController,
controller_rx: Receiver<TerminalRunEvent>,
debug_enabled: bool,
) {
#[cfg(test)]
{
let _ = (
app_event_tx,
terminal_id,
initial_command,
latest_version,
cwd,
controller,
controller_rx,
debug_enabled,
);
return;
}
start_guided_terminal_session(
app_event_tx,
terminal_id,
GuidedTerminalMode::Upgrade {
initial_command,
latest_version,
},
cwd,
controller,
controller_rx,
debug_enabled,
);
}
fn start_guided_terminal_session(
app_event_tx: AppEventSender,
terminal_id: u64,
@@ -130,9 +173,9 @@ fn start_guided_terminal_session(
Err(err) => {
let helper = match &mode {
GuidedTerminalMode::AgentInstall { .. } => "Install helper",
GuidedTerminalMode::Prompt { .. } | GuidedTerminalMode::DirectCommand { .. } => {
"Terminal helper"
}
GuidedTerminalMode::Prompt { .. }
| GuidedTerminalMode::DirectCommand { .. } => "Terminal helper",
GuidedTerminalMode::Upgrade { .. } => "Upgrade helper",
};
let msg = format!("Failed to start {helper} runtime: {err}");
app_event_tx.send(AppEvent::TerminalChunk {
@@ -161,9 +204,9 @@ fn start_guided_terminal_session(
) {
let helper = match &mode {
GuidedTerminalMode::AgentInstall { .. } => "Install helper",
GuidedTerminalMode::Prompt { .. } | GuidedTerminalMode::DirectCommand { .. } => {
"Terminal helper"
}
GuidedTerminalMode::Prompt { .. }
| GuidedTerminalMode::DirectCommand { .. } => "Terminal helper",
GuidedTerminalMode::Upgrade { .. } => "Upgrade helper",
};
let msg = if debug_enabled {
format!("{helper} error: {err:#}")
@@ -262,6 +305,25 @@ fn run_guided_loop(
),
"direct_terminal_flow",
),
GuidedTerminalMode::Upgrade {
initial_command,
latest_version,
} => {
let latest = latest_version
.as_ref()
.map(String::as_str)
.unwrap_or("unknown");
(
"Upgrade helper",
format!(
"You are helping the user upgrade Code to the latest available version. The preferred upgrade command is `{initial_command}`. Prioritize resolving permission prompts (including sudo password requests) and confirm the upgrade succeeds."
),
format!(
"Upgrade target: Code (latest version: {latest}).\nPrimary command: {initial_command}.\nPlatform: {platform}.\nSandbox: {sandbox}.\nWorking directory: {cwd_text}.\nRun the command, diagnose any failures (especially permissions), and guide the user until the upgrade completes."
),
"upgrade_terminal_flow",
)
}
};
if debug_enabled {
@@ -301,6 +363,20 @@ fn run_guided_loop(
cwd_text,
);
}
GuidedTerminalMode::Upgrade {
initial_command,
latest_version,
} => {
debug!(
"[{}] Starting upgrade session: command={} latest_version={:?} platform={} sandbox={} cwd={}",
helper_label,
initial_command,
latest_version,
platform,
sandbox,
cwd_text,
);
}
}
}
@@ -343,7 +419,7 @@ fn run_guided_loop(
conversation.push(make_message("user", initial_user));
let mut steps = match mode {
GuidedTerminalMode::DirectCommand { .. } => 1,
GuidedTerminalMode::DirectCommand { .. } | GuidedTerminalMode::Upgrade { .. } => 1,
_ => 0,
};
@@ -391,6 +467,50 @@ fn run_guided_loop(
id: terminal_id,
message: "Analyzing output…".to_string(),
});
} else if let GuidedTerminalMode::Upgrade { initial_command, .. } = mode {
let wrapped = wrap_command(initial_command);
if wrapped.is_empty() {
app_event_tx.send(AppEvent::TerminalChunk {
id: terminal_id,
chunk: b"Unable to build upgrade command for execution.\n".to_vec(),
_is_stderr: true,
});
app_event_tx.send(AppEvent::TerminalUpdateMessage {
id: terminal_id,
message: "Upgrade command could not be constructed.".to_string(),
});
return Ok(());
}
app_event_tx.send(AppEvent::TerminalRunCommand {
id: terminal_id,
command: wrapped,
command_display: initial_command.clone(),
controller: Some(controller.clone()),
});
let Some((output, exit_code)) = collect_command_output(controller_rx)
.context("collecting upgrade command output")?
else {
if debug_enabled {
debug!("[Upgrade helper] Initial command cancelled by user");
}
return Ok(());
};
let truncated = tail_chars(&output, MAX_OUTPUT_CHARS);
let summary = format!(
"Command: {initial_command}\nExit code: {}\nOutput (last {} chars):\n{}",
exit_code
.map(|c| c.to_string())
.unwrap_or_else(|| "unknown".to_string()),
truncated.chars().count(),
truncated
);
conversation.push(make_message("user", summary));
app_event_tx.send(AppEvent::TerminalSetAssistantMessage {
id: terminal_id,
message: "Analyzing output…".to_string(),
});
}
loop {
@@ -452,7 +572,9 @@ fn run_guided_loop(
let require_confirmation = match mode {
GuidedTerminalMode::AgentInstall { .. } => steps > 1,
GuidedTerminalMode::Prompt { .. } => steps > 1,
GuidedTerminalMode::DirectCommand { .. } => true,
GuidedTerminalMode::DirectCommand { .. } | GuidedTerminalMode::Upgrade { .. } => {
true
}
};
let final_command = if require_confirmation {
let (gate_tx, gate_rx) = channel();
@@ -244,6 +244,7 @@ pub(super) fn finalize_all_running_as_interrupted(chat: &mut ChatWidget<'_>) {
chat.bottom_pane.set_task_running(false);
}
chat.maybe_hide_spinner();
chat.refresh_auto_drive_visuals();
}
pub(super) fn finalize_all_running_due_to_answer(chat: &mut ChatWidget<'_>) {
@@ -364,6 +365,8 @@ pub(super) fn finalize_all_running_due_to_answer(chat: &mut ChatWidget<'_>) {
chat.tools_state.running_web_search.remove(&call_id);
}
}
chat.refresh_auto_drive_visuals();
}
pub(super) fn try_merge_completed_exec_at(chat: &mut ChatWidget<'_>, idx: usize) {
@@ -579,6 +582,7 @@ pub(super) fn handle_exec_begin_now(
_ => "exploring…",
};
chat.bottom_pane.update_status_text(status_text.to_string());
chat.refresh_auto_drive_visuals();
return;
}
}
@@ -633,6 +637,7 @@ pub(super) fn handle_exec_begin_now(
chat.bottom_pane
.update_status_text(format!("running command: {}", preview_short));
}
chat.refresh_auto_drive_visuals();
}
pub(super) fn handle_exec_end_now(
@@ -645,6 +650,7 @@ pub(super) fn handle_exec_end_now(
chat.exec.unsuppress_exec_end(&call_id);
chat.ended_call_ids.insert(call_id);
chat.maybe_hide_spinner();
chat.refresh_auto_drive_visuals();
return;
}
chat.ended_call_ids.insert(super::ExecCallId(ev.call_id.clone()));
@@ -655,6 +661,7 @@ pub(super) fn handle_exec_end_now(
.remove(&super::ExecCallId(ev.call_id.clone()))
{
chat.maybe_hide_spinner();
chat.refresh_auto_drive_visuals();
return;
}
let ExecCommandEndEvent {
@@ -738,6 +745,7 @@ pub(super) fn handle_exec_end_now(
};
chat.bottom_pane.update_status_text(status_text);
chat.maybe_hide_spinner();
chat.refresh_auto_drive_visuals();
return;
}
@@ -829,6 +837,7 @@ pub(super) fn handle_exec_end_now(
.update_status_text(format!("command failed (exit {})", exit_code));
}
chat.maybe_hide_spinner();
chat.refresh_auto_drive_visuals();
}
// Stable ordering now inserts at the correct position; these helpers are removed.
+64
View File
@@ -694,6 +694,32 @@ pub(crate) fn make_chatwidget_manual_with_sender() -> (
(widget, app_event_tx, rx, op_rx)
}
struct EnvGuard {
key: String,
prev: Option<String>,
}
impl EnvGuard {
fn set(key: &str, value: &str) -> Self {
let prev = std::env::var(key).ok();
std::env::set_var(key, value);
Self {
key: key.to_string(),
prev,
}
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
if let Some(prev) = self.prev.take() {
std::env::set_var(&self.key, prev);
} else {
std::env::remove_var(&self.key);
}
}
}
fn drain_insert_history(
rx: &std::sync::mpsc::Receiver<AppEvent>,
) -> Vec<Vec<ratatui::text::Line<'static>>> {
@@ -975,6 +1001,9 @@ fn pump_app_events(chat: &mut ChatWidget<'_>, rx: &std::sync::mpsc::Receiver<App
AppEvent::DispatchCommand(SlashCommand::Undo, _command_text) => {
chat.handle_undo_command();
}
AppEvent::DispatchCommand(SlashCommand::Update, command_text) => {
chat.handle_update_command(&command_text);
}
AppEvent::ShowUndoOptions { index } => {
chat.show_undo_restore_options(index);
}
@@ -1061,6 +1090,41 @@ fn slash_agents_opens_overview() {
);
}
#[test]
fn slash_upgrade_opens_guided_terminal() {
let _guard = EnvGuard::set("CODEX_MANAGED_BY_NPM", "1");
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
chat.latest_upgrade_version = Some("9.9.9".to_string());
let script = [
ScriptStep::key_char('/'),
ScriptStep::key_char('u'),
ScriptStep::key_char('p'),
ScriptStep::key_char('g'),
ScriptStep::key_char('r'),
ScriptStep::key_char('a'),
ScriptStep::key_char('d'),
ScriptStep::key_char('e'),
ScriptStep::enter(),
];
run_script(&mut chat, &script, &rx);
let mut saw_open_terminal = false;
while let Ok(event) = rx.try_recv() {
if let AppEvent::OpenTerminal(launch) = event {
saw_open_terminal = true;
assert_eq!(launch.title, "Upgrade Code");
assert!(
launch.command_display.contains("Guided"),
"expected guided terminal display, got {}",
launch.command_display
);
}
}
assert!(saw_open_terminal, "expected guided upgrade terminal to open");
}
#[test]
fn slash_undo_shows_no_snapshot_state() {
let (mut chat, rx, _op_rx) = make_chatwidget_manual();
+131
View File
@@ -6,6 +6,71 @@ use ratatui::buffer::Buffer;
use ratatui::layout::{Margin, Rect};
use ratatui::style::Color;
pub struct HeaderBorderWeaveEffect {
enabled: Cell<bool>,
started_at: Cell<Option<Instant>>,
next_frame: Cell<Option<Instant>>,
}
impl HeaderBorderWeaveEffect {
pub const FRAME_INTERVAL: Duration = HeaderWaveEffect::FRAME_INTERVAL;
pub fn new() -> Self {
Self {
enabled: Cell::new(false),
started_at: Cell::new(None),
next_frame: Cell::new(None),
}
}
pub fn set_enabled(&self, enabled: bool, now: Instant) {
self.enabled.set(enabled);
if enabled {
self.started_at.set(Some(now));
self.next_frame.set(None);
} else {
self.started_at.set(None);
self.next_frame.set(None);
}
}
pub fn is_enabled(&self) -> bool {
self.enabled.get()
}
pub fn schedule_if_needed(&self, now: Instant) -> bool {
if !self.enabled.get() {
return false;
}
match self.next_frame.get() {
Some(due) if due > now => false,
_ => {
self.next_frame
.set(Some(now + Self::FRAME_INTERVAL));
true
}
}
}
pub fn render(&self, area: Rect, buf: &mut Buffer, now: Instant) {
if !self.enabled.get() {
return;
}
if area.width < 2 || area.height < 2 {
return;
}
let start = self.started_at.get().unwrap_or_else(|| {
self.started_at.set(Some(now));
now
});
let elapsed = now.saturating_duration_since(start).as_secs_f32();
let tick_interval = Self::FRAME_INTERVAL.as_secs_f32().max(0.001);
let tick = (elapsed / tick_interval).floor() as u32;
let phase = wrap_unit(elapsed * 0.25);
render_border_weave(area, buf, phase, tick);
}
}
/// Declarative background animation used by the TUI status header.
pub struct HeaderWaveEffect {
enabled: Cell<bool>,
@@ -115,3 +180,69 @@ fn spectral_color(t: f32) -> Color {
)
}
fn render_border_weave(area: Rect, buf: &mut Buffer, phase: f32, tick: u32) {
let positions = border_positions(area);
if positions.is_empty() {
return;
}
let base_color = crate::colors::border();
let highlight = crate::colors::mix_toward(base_color, prism_color(phase), 0.75);
for (idx, &(x, y)) in positions.iter().enumerate() {
let tick_step = (tick as usize) % 12;
let phase_idx = (idx + (12 - tick_step)) % 12;
let color = if phase_idx < 4 {
highlight
} else if phase_idx < 8 {
crate::colors::mix_toward(highlight, base_color, 0.5)
} else {
crate::colors::mix_toward(base_color, crate::colors::text_dim(), 0.35)
};
let cell = &mut buf[(x, y)];
cell.set_fg(color);
cell.set_bg(crate::colors::background());
}
}
fn border_positions(area: Rect) -> Vec<(u16, u16)> {
if area.width < 2 || area.height < 2 {
return Vec::new();
}
let mut positions = Vec::new();
let x0 = area.x;
let x1 = area.x + area.width.saturating_sub(1);
let y0 = area.y;
let y1 = area.y + area.height.saturating_sub(1);
for x in x0..=x1 {
positions.push((x, y0));
}
if y1 > y0 {
for y in y0 + 1..=y1 {
positions.push((x1, y));
}
}
if x1 > x0 {
for x in (x0..x1).rev() {
positions.push((x, y1));
}
}
if y1 > y0 + 1 {
for y in (y0 + 1..y1).rev() {
positions.push((x0, y));
}
}
positions
}
fn prism_color(t: f32) -> Color {
let angle = wrap_unit(t) * TAU;
let r = (angle.sin() * 0.5 + 0.5).powf(0.55);
let g = ((angle + TAU / 3.0).sin() * 0.5 + 0.5).powf(0.55);
let b = ((angle + 2.0 * TAU / 3.0).sin() * 0.5 + 0.5).powf(0.55);
Color::Rgb(
(r * 255.0).clamp(0.0, 255.0) as u8,
(g * 255.0).clamp(0.0, 255.0) as u8,
(b * 255.0).clamp(0.0, 255.0) as u8,
)
}
+188 -24
View File
@@ -147,9 +147,15 @@ pub fn resolve_upgrade_resolution() -> UpgradeResolution {
}
}
pub async fn auto_upgrade_if_enabled(config: &Config) -> anyhow::Result<Option<String>> {
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct AutoUpgradeOutcome {
pub installed_version: Option<String>,
pub user_notice: Option<String>,
}
pub async fn auto_upgrade_if_enabled(config: &Config) -> anyhow::Result<AutoUpgradeOutcome> {
if !config.auto_upgrade_enabled {
return Ok(None);
return Ok(AutoUpgradeOutcome::default());
}
let resolution = resolve_upgrade_resolution();
@@ -160,7 +166,7 @@ pub async fn auto_upgrade_if_enabled(config: &Config) -> anyhow::Result<Option<S
} if !command.is_empty() => (command, command_display),
_ => {
info!("auto-upgrade enabled but no managed installer detected; skipping");
return Ok(None);
return Ok(AutoUpgradeOutcome::default());
}
};
@@ -168,24 +174,24 @@ pub async fn auto_upgrade_if_enabled(config: &Config) -> anyhow::Result<Option<S
Ok(info) => info,
Err(err) => {
warn!("auto-upgrade: failed to check for updates: {err}");
return Ok(None);
return Ok(AutoUpgradeOutcome::default());
}
};
let Some(latest_version) = info.latest_version.clone() else {
// Already up to date
return Ok(None);
return Ok(AutoUpgradeOutcome::default());
};
let lock = match AutoUpgradeLock::acquire(&config.codex_home) {
Ok(Some(lock)) => lock,
Ok(None) => {
info!("auto-upgrade already in progress by another instance; skipping");
return Ok(None);
return Ok(AutoUpgradeOutcome::default());
}
Err(err) => {
warn!("auto-upgrade: unable to acquire lock: {err}");
return Ok(None);
return Ok(AutoUpgradeOutcome::default());
}
};
@@ -194,17 +200,75 @@ pub async fn auto_upgrade_if_enabled(config: &Config) -> anyhow::Result<Option<S
latest_version = latest_version.as_str(),
"auto-upgrade: running managed installer"
);
let result = run_upgrade_command(command).await;
let result = execute_upgrade_command(&command).await;
drop(lock);
let mut outcome = AutoUpgradeOutcome {
installed_version: None,
user_notice: None,
};
match result {
Ok(()) => {
info!("auto-upgrade: successfully installed {latest_version}");
Ok(Some(latest_version))
Ok(primary) => {
if primary.success {
info!("auto-upgrade: successfully installed {latest_version}");
outcome.installed_version = Some(latest_version);
return Ok(outcome);
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
{
if !starts_with_sudo(&command) {
info!("auto-upgrade: retrying with sudo -n");
let sudo_command = wrap_with_sudo(&command);
match execute_upgrade_command(&sudo_command).await {
Ok(fallback) if fallback.success => {
info!("auto-upgrade: sudo retry succeeded; installed {latest_version}");
outcome.installed_version = Some(latest_version);
return Ok(outcome);
}
Ok(fallback) => {
if sudo_requires_manual_intervention(&fallback.stderr, fallback.status)
{
outcome.user_notice = Some(format!(
"Automatic upgrade needs your attention. Run `/upgrade` to finish with `{}`.",
command_display
));
}
warn!(
"auto-upgrade: sudo retry failed: status={:?} stderr={}",
fallback.status,
truncate_for_log(&fallback.stderr)
);
return Ok(outcome);
}
Err(err) => {
warn!("auto-upgrade: sudo retry error: {err}");
outcome.user_notice = Some(format!(
"Automatic upgrade could not escalate permissions. Run `/upgrade` to finish with `{}`.",
command_display
));
return Ok(outcome);
}
}
}
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
let _ = primary; // suppress unused warning on non-Unix targets
}
warn!(
"auto-upgrade: upgrade command failed: status={:?} stderr={}",
primary.status,
truncate_for_log(&primary.stderr)
);
Ok(outcome)
}
Err(err) => {
warn!("auto-upgrade: upgrade command failed: {err}");
Ok(None)
warn!("auto-upgrade: failed to launch upgrade command: {err}");
Ok(outcome)
}
}
}
@@ -288,7 +352,14 @@ impl Drop for AutoUpgradeLock {
}
}
async fn run_upgrade_command(command: Vec<String>) -> anyhow::Result<()> {
#[derive(Debug, Clone)]
struct CommandCapture {
success: bool,
status: Option<i32>,
stderr: String,
}
async fn execute_upgrade_command(command: &[String]) -> anyhow::Result<CommandCapture> {
if command.is_empty() {
anyhow::bail!("upgrade command is empty");
}
@@ -297,18 +368,111 @@ async fn run_upgrade_command(command: Vec<String>) -> anyhow::Result<()> {
if command.len() > 1 {
cmd.args(&command[1..]);
}
cmd.stdin(Stdio::null());
cmd.stdout(Stdio::null());
cmd.stderr(Stdio::null());
let status = cmd.status().await?;
if !status.success() {
anyhow::bail!(
"upgrade command exited with status {}",
status.code().map_or_else(|| "signal".to_string(), |c| c.to_string())
);
cmd.stdin(Stdio::null());
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let output = cmd.output().await?;
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
Ok(CommandCapture {
success: output.status.success(),
status: output.status.code(),
stderr,
})
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn wrap_with_sudo(command: &[String]) -> Vec<String> {
let mut out = Vec::with_capacity(command.len() + 3);
out.push("sudo".to_string());
out.push("-n".to_string());
out.push("--".to_string());
out.extend(command.iter().cloned());
out
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn starts_with_sudo(command: &[String]) -> bool {
command
.first()
.map(|c| c.eq_ignore_ascii_case("sudo"))
.unwrap_or(false)
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn sudo_requires_manual_intervention(stderr: &str, status: Option<i32>) -> bool {
let lowered = stderr.to_ascii_lowercase();
let needs_password = lowered.contains("password is required")
|| lowered.contains("a password is required")
|| lowered.contains("no tty present and no askpass program specified")
|| lowered.contains("must be run from a terminal")
|| lowered.contains("may not run sudo")
|| lowered.contains("permission denied");
needs_password && status == Some(1)
}
fn truncate_for_log(text: &str) -> String {
const LIMIT: usize = 256;
if text.len() <= LIMIT {
text.replace('\n', " ")
} else {
let mut truncated = text[..LIMIT].to_string();
truncated.push_str("…");
truncated.replace('\n', " ")
}
}
#[cfg(test)]
mod unit_tests {
use super::*;
#[test]
fn truncate_for_log_short_text_preserved() {
let text = "no issues";
assert_eq!(truncate_for_log(text), text);
}
#[test]
fn truncate_for_log_truncates_long_messages() {
let long = "a".repeat(300);
let truncated = truncate_for_log(&long);
assert!(truncated.ends_with('…'));
assert!(truncated.len() <= 257);
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[test]
fn wrap_with_sudo_injects_dash_dash() {
let original = vec!["npm".to_string(), "install".to_string()];
let wrapped = wrap_with_sudo(&original);
assert_eq!(wrapped[0], "sudo");
assert_eq!(wrapped[1], "-n");
assert_eq!(wrapped[2], "--");
assert_eq!(&wrapped[3..], original);
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[test]
fn starts_with_sudo_detects_prefix_case_insensitive() {
assert!(starts_with_sudo(&["sudo".into(), "true".into()]));
assert!(starts_with_sudo(&["SUDO".into(), "true".into()]));
assert!(!starts_with_sudo(&["npm".into(), "install".into()]));
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[test]
fn sudo_requires_manual_intervention_matches_common_errors() {
let stderr = "sudo: a password is required";
assert!(sudo_requires_manual_intervention(stderr, Some(1)));
let stderr_case = "sudo: Permission denied";
assert!(sudo_requires_manual_intervention(stderr_case, Some(1)));
let stderr_other = "sudo: authentication succeeded";
assert!(!sudo_requires_manual_intervention(stderr_other, Some(0)));
}
Ok(())
}
fn read_version_info(version_file: &Path) -> anyhow::Result<VersionInfo> {
+2 -2
View File
@@ -47,8 +47,8 @@ Notes
- `/prompts`: show example prompts.
- `/status`: show current session configuration and token usage.
- `/limits`: visualize current hourly and weekly rate-limit usage.
- `/update`: check the installed version, detect available upgrades, and run the
appropriate installer when possible.
- `/update`: check the installed version, detect available upgrades, and open a
guided upgrade terminal that runs the installer interactively when possible.
- `/notifications`: inspect and toggle TUI notifications.
- `/mcp`: manage MCP servers (status/on/off/add).
- `/validation [status|on|off|<tool> (on|off)]`: inspect or toggle validation
+5 -8
View File
@@ -1,14 +1,11 @@
## @just-every/code v0.2.170
## @just-every/code v0.2.173
This update modernizes the TUI event pipeline and shores up notifications, quotas, and account workflows.
This release tightens the TUI browser loop so failures hand off to Code with clearer diagnostics.
### Changes
- TUI/History: drive exec, assistant, explore, and rate-limit cells from domain events for consistent streaming.
- TUI/Notifications: add an OSC toggle command and harden slash routing, persistence, and filters so alerts stay accurate.
- Usage/Rate-limits: compact persisted stats, relog after resets, and persist reset state to keep quotas current.
- TUI/Accounts: prioritize ChatGPT accounts in login flows and restore the label prefix for clarity.
- UX: show a session resume hint on exit, surface Zed model selection, and restore Option+Enter newline plus Cmd+Z undo.
- TUI/Browser: auto hand off /browser startup failures to Code so sessions self-heal.
- TUI/Browser: sanitize and surface error details when handoff triggers for faster diagnosis.
### Install
@@ -17,4 +14,4 @@ npm install -g @just-every/code@latest
code
```
Compare: https://github.com/just-every/code/compare/v0.2.169...v0.2.170
Compare: https://github.com/just-every/code/compare/v0.2.172...v0.2.173