mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
merge local main into code-branch-20250918-102707 before merge
This commit is contained in:
@@ -634,6 +634,7 @@ struct State {
|
||||
current_task: Option<AgentTask>,
|
||||
pending_approvals: HashMap<String, oneshot::Sender<ReviewDecision>>,
|
||||
pending_input: Vec<ResponseInputItem>,
|
||||
pending_user_input: Vec<QueuedUserInput>,
|
||||
history: ConversationHistory,
|
||||
/// Tracks which completed agents (by id) have already been returned to the
|
||||
/// model for a given batch when using `agent_wait` without `return_all`.
|
||||
@@ -650,6 +651,13 @@ struct State {
|
||||
next_internal_sub_id: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct QueuedUserInput {
|
||||
submission_id: String,
|
||||
response_item: ResponseInputItem,
|
||||
core_items: Vec<InputItem>,
|
||||
}
|
||||
|
||||
/// Buffers partial turn progress produced during a single HTTP streaming attempt.
|
||||
/// This is not recorded to persistent history. It is only used to seed retries
|
||||
/// when the SSE stream disconnects mid‑turn.
|
||||
@@ -846,6 +854,24 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_running_task(&self) -> bool {
|
||||
self.state.lock().unwrap().current_task.is_some()
|
||||
}
|
||||
|
||||
pub fn queue_user_input(&self, queued: QueuedUserInput) {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
state.pending_user_input.push(queued);
|
||||
}
|
||||
|
||||
pub fn pop_next_queued_user_input(&self) -> Option<QueuedUserInput> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
if state.pending_user_input.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(state.pending_user_input.remove(0))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn next_internal_sub_id(&self) -> String {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
let id = state.next_internal_sub_id;
|
||||
@@ -1566,11 +1592,25 @@ impl Session {
|
||||
|
||||
pub fn get_pending_input(&self) -> Vec<ResponseInputItem> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
if state.pending_input.is_empty() {
|
||||
if state.pending_input.is_empty() && state.pending_user_input.is_empty() {
|
||||
Vec::with_capacity(0)
|
||||
} else {
|
||||
let mut ret = Vec::new();
|
||||
std::mem::swap(&mut ret, &mut state.pending_input);
|
||||
if !state.pending_input.is_empty() {
|
||||
let mut model_inputs = Vec::new();
|
||||
std::mem::swap(&mut model_inputs, &mut state.pending_input);
|
||||
ret.extend(model_inputs);
|
||||
}
|
||||
|
||||
if !state.pending_user_input.is_empty() {
|
||||
let mut queued_user_inputs = Vec::new();
|
||||
std::mem::swap(&mut queued_user_inputs, &mut state.pending_user_input);
|
||||
ret.extend(
|
||||
queued_user_inputs
|
||||
.into_iter()
|
||||
.map(|queued| queued.response_item),
|
||||
);
|
||||
}
|
||||
ret
|
||||
}
|
||||
}
|
||||
@@ -2155,6 +2195,31 @@ async fn submission_loop(
|
||||
let agent = AgentTask::spawn(Arc::clone(&sess), turn_context, sub.id.clone(), items);
|
||||
sess.set_task(agent);
|
||||
}
|
||||
Op::QueueUserInput { items } => {
|
||||
let sess = match sess.as_ref() {
|
||||
Some(sess) => sess,
|
||||
None => {
|
||||
send_no_session_event(sub.id).await;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if sess.has_running_task() {
|
||||
let response_item = response_input_from_core_items(items.clone());
|
||||
let queued = QueuedUserInput {
|
||||
submission_id: sub.id.clone(),
|
||||
response_item,
|
||||
core_items: items,
|
||||
};
|
||||
sess.queue_user_input(queued);
|
||||
} else {
|
||||
// No task running: treat this as immediate user input without aborting.
|
||||
sess.cleanup_old_status_items().await;
|
||||
let turn_context = sess.make_turn_context();
|
||||
let agent = AgentTask::spawn(Arc::clone(&sess), turn_context, sub.id.clone(), items);
|
||||
sess.set_task(agent);
|
||||
}
|
||||
}
|
||||
Op::ExecApproval { id, decision } => {
|
||||
let sess = match sess.as_ref() {
|
||||
Some(sess) => sess,
|
||||
@@ -2546,6 +2611,18 @@ async fn run_agent(sess: Arc<Session>, turn_context: Arc<TurnContext>, sub_id: S
|
||||
_ => {}
|
||||
}
|
||||
sess.tx_event.send(event).await.ok();
|
||||
|
||||
if let Some(queued) = sess.pop_next_queued_user_input() {
|
||||
let sess_clone = Arc::clone(&sess);
|
||||
tokio::spawn(async move {
|
||||
sess_clone.cleanup_old_status_items().await;
|
||||
let turn_context = sess_clone.make_turn_context();
|
||||
let submission_id = queued.submission_id;
|
||||
let items = queued.core_items;
|
||||
let agent = AgentTask::spawn(Arc::clone(&sess_clone), turn_context, submission_id, items);
|
||||
sess_clone.set_task(agent);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_turn(
|
||||
|
||||
@@ -105,6 +105,13 @@ pub enum Op {
|
||||
items: Vec<InputItem>,
|
||||
},
|
||||
|
||||
/// Queue user input to be appended to the next model request without
|
||||
/// interrupting the current turn.
|
||||
QueueUserInput {
|
||||
/// User input items, see `InputItem`
|
||||
items: Vec<InputItem>,
|
||||
},
|
||||
|
||||
/// Approve a command execution
|
||||
ExecApproval {
|
||||
/// The id of the submission we are approving
|
||||
|
||||
@@ -61,6 +61,13 @@ pub enum Op {
|
||||
items: Vec<InputItem>,
|
||||
},
|
||||
|
||||
/// Queue user input to be appended to the next model request without
|
||||
/// interrupting the current turn.
|
||||
QueueUserInput {
|
||||
/// User input items, see `InputItem`
|
||||
items: Vec<InputItem>,
|
||||
},
|
||||
|
||||
/// Similar to [`Op::UserInput`], but contains additional context required
|
||||
/// for a turn of a [`crate::codex_conversation::CodexConversation`].
|
||||
UserTurn {
|
||||
|
||||
@@ -57,7 +57,7 @@ pub enum InputResult {
|
||||
}
|
||||
|
||||
struct TokenUsageInfo {
|
||||
total_token_usage: TokenUsage,
|
||||
_total_token_usage: TokenUsage,
|
||||
last_token_usage: TokenUsage,
|
||||
model_context_window: Option<u64>,
|
||||
/// Baseline token count present in the context before the user's first
|
||||
@@ -476,7 +476,7 @@ impl ChatComposer {
|
||||
.unwrap_or_else(|| last_token_usage.cached_input_tokens.unwrap_or(0));
|
||||
|
||||
self.token_usage_info = Some(TokenUsageInfo {
|
||||
total_token_usage,
|
||||
_total_token_usage: total_token_usage,
|
||||
last_token_usage,
|
||||
model_context_window,
|
||||
initial_prompt_tokens,
|
||||
@@ -656,6 +656,10 @@ impl ChatComposer {
|
||||
self.sync_file_search_popup();
|
||||
}
|
||||
|
||||
pub(crate) fn text(&self) -> &str {
|
||||
self.textarea.text()
|
||||
}
|
||||
|
||||
/// Close the file-search popup if it is currently active. Returns true if closed.
|
||||
pub(crate) fn close_file_popup_if_active(&mut self) -> bool {
|
||||
match self.active_popup {
|
||||
@@ -1628,22 +1632,24 @@ impl WidgetRef for ChatComposer {
|
||||
// Prepare token usage spans (always shown when available)
|
||||
let mut token_spans: Vec<Span> = Vec::new();
|
||||
if let Some(token_usage_info) = &self.token_usage_info {
|
||||
let token_usage = &token_usage_info.total_token_usage;
|
||||
let used_str = format_with_thousands(token_usage.blended_total());
|
||||
let turn_usage = &token_usage_info.last_token_usage;
|
||||
let turn_tokens = turn_usage.blended_total();
|
||||
let used_str = format_with_thousands(turn_tokens);
|
||||
token_spans.push(Span::from(used_str).style(label_style.add_modifier(Modifier::BOLD)));
|
||||
token_spans.push(Span::from(" tokens ").style(label_style));
|
||||
token_spans.push(Span::from(" tokens").style(label_style));
|
||||
if let Some(context_window) = token_usage_info.model_context_window {
|
||||
let last_token_usage = &token_usage_info.last_token_usage;
|
||||
let percent_remaining: u8 = if context_window > 0 {
|
||||
let percent = 100.0
|
||||
- (last_token_usage.tokens_in_context_window() as f32
|
||||
/ context_window as f32
|
||||
* 100.0);
|
||||
percent.clamp(0.0, 100.0) as u8
|
||||
} else { 100 };
|
||||
token_spans.push(Span::from("(").style(label_style));
|
||||
token_spans.push(Span::from(percent_remaining.to_string()).style(label_style.add_modifier(Modifier::BOLD)));
|
||||
token_spans.push(Span::from("% left)").style(label_style));
|
||||
if context_window > 0 {
|
||||
let percent_remaining = {
|
||||
let percent = 100.0
|
||||
- (turn_usage.tokens_in_context_window() as f32
|
||||
/ context_window as f32
|
||||
* 100.0);
|
||||
percent.clamp(0.0, 100.0) as u8
|
||||
};
|
||||
token_spans.push(Span::from(" (").style(label_style));
|
||||
token_spans.push(Span::from(percent_remaining.to_string()).style(label_style.add_modifier(Modifier::BOLD)));
|
||||
token_spans.push(Span::from("% left)").style(label_style));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -407,6 +407,10 @@ impl BottomPane<'_> {
|
||||
self.composer.is_empty()
|
||||
}
|
||||
|
||||
pub(crate) fn composer_text(&self) -> String {
|
||||
self.composer.text().to_string()
|
||||
}
|
||||
|
||||
pub(crate) fn is_task_running(&self) -> bool {
|
||||
self.is_task_running
|
||||
}
|
||||
|
||||
+194
-88
@@ -293,6 +293,7 @@ pub(crate) struct ChatWidget<'a> {
|
||||
// point we submit one queued message and move its cell into the
|
||||
// normal history within the new turn window.
|
||||
queued_user_messages: std::collections::VecDeque<UserMessage>,
|
||||
pending_dispatched_user_messages: std::collections::VecDeque<String>,
|
||||
// Number of user prompts we pre-pended to history just before starting
|
||||
// a new turn; used to anchor the next turn window so assistant output
|
||||
// appears after them.
|
||||
@@ -1135,6 +1136,14 @@ impl ChatWidget<'_> {
|
||||
if text.is_empty() {
|
||||
return;
|
||||
}
|
||||
if role == "user" {
|
||||
if let Some(expected) = self.pending_dispatched_user_messages.front() {
|
||||
if expected.trim() == text {
|
||||
self.pending_dispatched_user_messages.pop_front();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if text.starts_with("== System Status ==") {
|
||||
return;
|
||||
}
|
||||
@@ -1150,6 +1159,13 @@ impl ChatWidget<'_> {
|
||||
Box::new(crate::history_cell::new_user_prompt(text.to_string())),
|
||||
key,
|
||||
);
|
||||
|
||||
if let Some(front) = self.queued_user_messages.front() {
|
||||
if front.display_text.trim() == text.trim() {
|
||||
self.queued_user_messages.pop_front();
|
||||
self.refresh_queued_user_messages();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
use crate::history_cell::HistoryCellType;
|
||||
use crate::history_cell::PlainHistoryCell;
|
||||
@@ -1762,20 +1778,33 @@ impl ChatWidget<'_> {
|
||||
// Restore any queued messages back into the composer so the user can
|
||||
// immediately press Enter to resume the conversation where they left off.
|
||||
if !self.queued_user_messages.is_empty() {
|
||||
let mut prefill = String::new();
|
||||
let existing_input = self.bottom_pane.composer_text();
|
||||
let mut segments: Vec<String> = Vec::new();
|
||||
|
||||
let mut queued_block = String::new();
|
||||
for (i, qm) in self.queued_user_messages.iter().enumerate() {
|
||||
if i > 0 {
|
||||
prefill.push('\n');
|
||||
queued_block.push_str("\n\n");
|
||||
}
|
||||
prefill.push_str(qm.display_text.trim_end());
|
||||
queued_block.push_str(qm.display_text.trim_end());
|
||||
}
|
||||
if !queued_block.trim().is_empty() {
|
||||
segments.push(queued_block);
|
||||
}
|
||||
|
||||
if !existing_input.trim().is_empty() {
|
||||
segments.push(existing_input);
|
||||
}
|
||||
|
||||
let combined = segments.join("\n\n");
|
||||
self.clear_composer();
|
||||
if !prefill.is_empty() {
|
||||
self.insert_str(&prefill);
|
||||
if !combined.is_empty() {
|
||||
self.insert_str(&combined);
|
||||
}
|
||||
self.queued_user_messages.clear();
|
||||
// Clear any sticky status like "queued for next turn" now that we returned text
|
||||
self.bottom_pane.update_status_text(String::new());
|
||||
self.pending_dispatched_user_messages.clear();
|
||||
self.refresh_queued_user_messages();
|
||||
}
|
||||
self.maybe_hide_spinner();
|
||||
self.request_redraw();
|
||||
@@ -1987,6 +2016,7 @@ impl ChatWidget<'_> {
|
||||
pending_jump_back: None,
|
||||
active_task_ids: HashSet::new(),
|
||||
queued_user_messages: std::collections::VecDeque::new(),
|
||||
pending_dispatched_user_messages: std::collections::VecDeque::new(),
|
||||
pending_user_prompts_for_next_turn: 0,
|
||||
browser_is_external: false,
|
||||
// Stable ordering & routing init
|
||||
@@ -2174,6 +2204,7 @@ impl ChatWidget<'_> {
|
||||
pending_jump_back: None,
|
||||
active_task_ids: HashSet::new(),
|
||||
queued_user_messages: std::collections::VecDeque::new(),
|
||||
pending_dispatched_user_messages: std::collections::VecDeque::new(),
|
||||
pending_user_prompts_for_next_turn: 0,
|
||||
browser_is_external: false,
|
||||
// Strict ordering init for forked widget
|
||||
@@ -2963,10 +2994,7 @@ impl ChatWidget<'_> {
|
||||
for cell in &self.history_cells {
|
||||
cell.trigger_fade();
|
||||
}
|
||||
let UserMessage {
|
||||
display_text,
|
||||
mut ordered_items,
|
||||
} = user_message;
|
||||
let mut message = user_message;
|
||||
// If our configured cwd no longer exists (e.g., a worktree folder was
|
||||
// deleted outside the app), try to automatically recover to the repo
|
||||
// root for worktrees and re-submit the same message there.
|
||||
@@ -2993,7 +3021,7 @@ impl ChatWidget<'_> {
|
||||
// Re-submit this exact message after switching cwd
|
||||
self.app_event_tx.send(AppEvent::SwitchCwd(
|
||||
fallback_root,
|
||||
Some(display_text.clone()),
|
||||
Some(message.display_text.clone()),
|
||||
));
|
||||
return;
|
||||
}
|
||||
@@ -3005,10 +3033,10 @@ impl ChatWidget<'_> {
|
||||
)));
|
||||
return;
|
||||
}
|
||||
let original_text = display_text.clone();
|
||||
let original_text = message.display_text.clone();
|
||||
// Build a combined string view of the text-only parts to process slash commands
|
||||
let mut text_only = String::new();
|
||||
for it in &ordered_items {
|
||||
for it in &message.ordered_items {
|
||||
if let InputItem::Text { text } = it {
|
||||
if !text_only.is_empty() {
|
||||
text_only.push('\n');
|
||||
@@ -3078,8 +3106,12 @@ impl ChatWidget<'_> {
|
||||
kind: crate::history_cell::HistoryCellType::Notice,
|
||||
});
|
||||
|
||||
ordered_items.clear();
|
||||
ordered_items.push(InputItem::Text { text: res.prompt });
|
||||
message
|
||||
.ordered_items
|
||||
.clear();
|
||||
message
|
||||
.ordered_items
|
||||
.push(InputItem::Text { text: res.prompt });
|
||||
// Continue with normal submission after this match block
|
||||
}
|
||||
}
|
||||
@@ -3130,13 +3162,21 @@ impl ChatWidget<'_> {
|
||||
});
|
||||
|
||||
// Replace the message with the resolved prompt
|
||||
ordered_items.clear();
|
||||
ordered_items.push(InputItem::Text { text: res.prompt });
|
||||
message
|
||||
.ordered_items
|
||||
.clear();
|
||||
message
|
||||
.ordered_items
|
||||
.push(InputItem::Text { text: res.prompt });
|
||||
} else {
|
||||
// Fallback to default expansion behavior
|
||||
let expanded = _expanded;
|
||||
ordered_items.clear();
|
||||
ordered_items.push(InputItem::Text { text: expanded });
|
||||
message
|
||||
.ordered_items
|
||||
.clear();
|
||||
message
|
||||
.ordered_items
|
||||
.push(InputItem::Text { text: expanded });
|
||||
}
|
||||
}
|
||||
crate::slash_command::ProcessedCommand::RegularCommand(cmd, _args) => {
|
||||
@@ -3298,18 +3338,131 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
|
||||
// Use the ordered items (text + images interleaved with markers)
|
||||
items.extend(ordered_items);
|
||||
items.extend(message.ordered_items.clone());
|
||||
message.ordered_items = items;
|
||||
|
||||
if items.is_empty() {
|
||||
if message.ordered_items.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Debug logging for what we're sending
|
||||
let ephemeral_count = items
|
||||
let turn_active = self.is_task_running()
|
||||
|| !self.active_task_ids.is_empty()
|
||||
|| self.stream.is_write_cycle_active()
|
||||
|| !self.queued_user_messages.is_empty();
|
||||
|
||||
if turn_active {
|
||||
tracing::info!(
|
||||
"Queuing user input while turn is active (queued: {})",
|
||||
self.queued_user_messages.len() + 1
|
||||
);
|
||||
self.queued_user_messages.push_back(message);
|
||||
self.refresh_queued_user_messages();
|
||||
|
||||
let queue_items = self
|
||||
.queued_user_messages
|
||||
.back()
|
||||
.map(|msg| msg.ordered_items.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
match self.codex_op_tx.send(Op::QueueUserInput { items: queue_items }) {
|
||||
Ok(()) => {
|
||||
if let Some(sent_message) = self.queued_user_messages.pop_back() {
|
||||
self.refresh_queued_user_messages();
|
||||
self.finalize_sent_user_message(sent_message);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("failed to send QueueUserInput op: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let mut batch: Vec<UserMessage> = self.queued_user_messages.drain(..).collect();
|
||||
batch.push(message);
|
||||
self.refresh_queued_user_messages();
|
||||
self.send_user_messages_to_agent(batch);
|
||||
|
||||
// (debug watchdog removed)
|
||||
}
|
||||
|
||||
fn flush_pending_agent_notes(&mut self) {
|
||||
for note in self.pending_agent_notes.drain(..) {
|
||||
if let Err(e) = self.codex_op_tx.send(Op::AddToHistory { text: note }) {
|
||||
tracing::error!("failed to send AddToHistory op: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn finalize_sent_user_message(&mut self, message: UserMessage) {
|
||||
let UserMessage {
|
||||
display_text,
|
||||
..
|
||||
} = message;
|
||||
|
||||
if !display_text.is_empty() {
|
||||
self.history_push_prompt_next_req(history_cell::new_user_prompt(display_text.clone()));
|
||||
self.pending_user_prompts_for_next_turn =
|
||||
self.pending_user_prompts_for_next_turn.saturating_add(1);
|
||||
self.pending_dispatched_user_messages
|
||||
.push_back(display_text.clone());
|
||||
}
|
||||
|
||||
self.flush_pending_agent_notes();
|
||||
|
||||
if !display_text.is_empty() {
|
||||
if let Err(e) = self
|
||||
.codex_op_tx
|
||||
.send(Op::AddToHistory { text: display_text })
|
||||
{
|
||||
tracing::error!("failed to send AddHistory op: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
fn send_user_messages_to_agent(&mut self, messages: Vec<UserMessage>) {
|
||||
if messages.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut combined_items: Vec<InputItem> = Vec::new();
|
||||
let mut history_texts: Vec<String> = Vec::new();
|
||||
|
||||
for (idx, UserMessage {
|
||||
display_text,
|
||||
ordered_items,
|
||||
}) in messages.into_iter().enumerate()
|
||||
{
|
||||
if !display_text.is_empty() {
|
||||
self.history_push_prompt_next_req(history_cell::new_user_prompt(
|
||||
display_text.clone(),
|
||||
));
|
||||
self.pending_user_prompts_for_next_turn =
|
||||
self.pending_user_prompts_for_next_turn.saturating_add(1);
|
||||
history_texts.push(display_text.clone());
|
||||
}
|
||||
|
||||
if idx > 0 && !combined_items.is_empty() && !ordered_items.is_empty() {
|
||||
combined_items.push(InputItem::Text {
|
||||
text: "\n\n".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
combined_items.extend(ordered_items);
|
||||
}
|
||||
|
||||
if combined_items.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let total_items = combined_items.len();
|
||||
let ephemeral_count = combined_items
|
||||
.iter()
|
||||
.filter(|item| matches!(item, InputItem::EphemeralImage { .. }))
|
||||
.count();
|
||||
let total_items = items.len();
|
||||
if ephemeral_count > 0 {
|
||||
tracing::info!(
|
||||
"Sending {} items to model (including {} ephemeral images)",
|
||||
@@ -3318,40 +3471,26 @@ impl ChatWidget<'_> {
|
||||
);
|
||||
}
|
||||
|
||||
// New policy: always send immediately even if a task is running.
|
||||
// The core will abort any current task on UserInput and start a new turn.
|
||||
// This prevents messages from being stranded in `queued_user_messages`
|
||||
// after an interrupt (there is no TaskComplete after Op::Interrupt).
|
||||
if self.bottom_pane.is_task_running() || !self.active_task_ids.is_empty() {
|
||||
self.bottom_pane
|
||||
.update_status_text("interrupting & starting new turn".to_string());
|
||||
self.flush_pending_agent_notes();
|
||||
|
||||
if let Err(e) = self
|
||||
.codex_op_tx
|
||||
.send(Op::UserInput {
|
||||
items: combined_items,
|
||||
})
|
||||
{
|
||||
tracing::error!("failed to send Op::UserInput: {e}");
|
||||
}
|
||||
|
||||
// Idle path: append the user cell first so the upcoming TaskStarted
|
||||
// window begins after it, ensuring assistant output renders below.
|
||||
if !original_text.is_empty() {
|
||||
self.history_push_prompt_next_req(history_cell::new_user_prompt(original_text.clone()));
|
||||
self.pending_user_prompts_for_next_turn =
|
||||
self.pending_user_prompts_for_next_turn.saturating_add(1);
|
||||
for text in history_texts {
|
||||
if let Err(e) = self.codex_op_tx.send(Op::AddToHistory { text }) {
|
||||
tracing::error!("failed to send AddHistory op: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If an access-mode change was pending, record it in the agent's
|
||||
// conversation history right before the next user turn.
|
||||
for note in self.pending_agent_notes.drain(..) {
|
||||
let _ = self.codex_op_tx.send(Op::AddToHistory { text: note });
|
||||
}
|
||||
|
||||
// Now send to the agent
|
||||
let _ = self.codex_op_tx.send(Op::UserInput { items });
|
||||
if !original_text.is_empty() {
|
||||
self.codex_op_tx
|
||||
.send(Op::AddToHistory {
|
||||
text: original_text.clone(),
|
||||
})
|
||||
.unwrap_or_else(|e| tracing::error!("failed to send AddHistory op: {e}"));
|
||||
}
|
||||
|
||||
// (debug watchdog removed)
|
||||
fn refresh_queued_user_messages(&mut self) {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -3759,40 +3898,6 @@ impl ChatWidget<'_> {
|
||||
// Now that streaming is complete, flush any queued interrupts
|
||||
self.flush_interrupt_queue();
|
||||
|
||||
// If any user messages were queued while the previous task was
|
||||
// running, dispatch exactly one now to start the next turn.
|
||||
if let Some(next) = self.queued_user_messages.pop_front() {
|
||||
// Move it from the sticky queue preview into history
|
||||
let UserMessage {
|
||||
display_text,
|
||||
ordered_items,
|
||||
} = next;
|
||||
if !display_text.is_empty() {
|
||||
self.history_push_prompt_next_req(history_cell::new_user_prompt(
|
||||
display_text.clone(),
|
||||
));
|
||||
self.pending_user_prompts_for_next_turn =
|
||||
self.pending_user_prompts_for_next_turn.saturating_add(1);
|
||||
}
|
||||
self.codex_op_tx
|
||||
.send(Op::UserInput {
|
||||
items: ordered_items,
|
||||
})
|
||||
.unwrap_or_else(|e| tracing::error!("failed to send queued message: {e}"));
|
||||
if !display_text.is_empty() {
|
||||
self.codex_op_tx
|
||||
.send(Op::AddToHistory { text: display_text })
|
||||
.unwrap_or_else(|e| {
|
||||
tracing::error!("failed to send AddHistory for queued: {e}")
|
||||
});
|
||||
}
|
||||
// Keep the spinner up; TaskStarted will follow.
|
||||
self.bottom_pane.set_task_running(true);
|
||||
self.bottom_pane
|
||||
.update_status_text("waiting for model".to_string());
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
// Only drop the working status if nothing is actually running.
|
||||
let any_tools_running = !self.exec.running_commands.is_empty()
|
||||
|| !self.tools_state.running_custom_tools.is_empty()
|
||||
@@ -4559,6 +4664,7 @@ impl ChatWidget<'_> {
|
||||
self.history_push(history_cell::new_status_output(
|
||||
&self.config,
|
||||
&self.total_token_usage,
|
||||
&self.last_token_usage,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -544,7 +544,7 @@ fn interrupt_restores_queued_messages_into_composer() {
|
||||
// Composer should now contain the queued messages joined by newlines, in order.
|
||||
assert_eq!(
|
||||
chat.bottom_pane.composer_text(),
|
||||
"first queued\nsecond queued"
|
||||
"first queued\n\nsecond queued"
|
||||
);
|
||||
|
||||
// Queue should be cleared and no new user input should have been auto-submitted.
|
||||
|
||||
@@ -19,6 +19,7 @@ use codex_core::protocol::FileChange;
|
||||
use codex_core::protocol::McpInvocation;
|
||||
use codex_core::protocol::SessionConfiguredEvent;
|
||||
use codex_core::protocol::TokenUsage;
|
||||
use codex_protocol::num_format::format_with_separators;
|
||||
use image::DynamicImage;
|
||||
use image::ImageReader;
|
||||
use mcp_types::EmbeddedResourceResource;
|
||||
@@ -8843,7 +8844,11 @@ pub(crate) fn new_model_output(model: &str, effort: ReasoningEffort) -> PlainHis
|
||||
|
||||
// Continue with more factory functions...
|
||||
// I'll add the rest in the next part to keep this manageable
|
||||
pub(crate) fn new_status_output(config: &Config, usage: &TokenUsage) -> PlainHistoryCell {
|
||||
pub(crate) fn new_status_output(
|
||||
config: &Config,
|
||||
total_usage: &TokenUsage,
|
||||
last_usage: &TokenUsage,
|
||||
) -> PlainHistoryCell {
|
||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||||
|
||||
lines.push(Line::from("/status").fg(crate::colors::keyword()));
|
||||
@@ -8960,24 +8965,77 @@ pub(crate) fn new_status_output(config: &Config, usage: &TokenUsage) -> PlainHis
|
||||
// Input: <input> [+ <cached> cached]
|
||||
let mut input_line_spans: Vec<Span<'static>> = vec![
|
||||
" • Input: ".into(),
|
||||
usage.non_cached_input().to_string().into(),
|
||||
format_with_separators(last_usage.non_cached_input()).into(),
|
||||
];
|
||||
if let Some(cached) = usage.cached_input_tokens {
|
||||
if let Some(cached) = last_usage.cached_input_tokens {
|
||||
if cached > 0 {
|
||||
input_line_spans.push(format!(" (+ {cached} cached)").into());
|
||||
input_line_spans.push(format!(" (+ {} cached)", format_with_separators(cached)).into());
|
||||
}
|
||||
}
|
||||
lines.push(Line::from(input_line_spans));
|
||||
// Output: <output>
|
||||
lines.push(Line::from(vec![
|
||||
" • Output: ".into(),
|
||||
usage.output_tokens.to_string().into(),
|
||||
format_with_separators(last_usage.output_tokens).into(),
|
||||
]));
|
||||
// Total: <total>
|
||||
lines.push(Line::from(vec![
|
||||
" • Total: ".into(),
|
||||
usage.blended_total().to_string().into(),
|
||||
format_with_separators(last_usage.blended_total()).into(),
|
||||
]));
|
||||
lines.push(Line::from(vec![
|
||||
" • Session total: ".into(),
|
||||
format_with_separators(total_usage.blended_total()).into(),
|
||||
]));
|
||||
|
||||
// 📐 Model Limits
|
||||
let context_window = config.model_context_window;
|
||||
let max_output_tokens = config.model_max_output_tokens;
|
||||
let auto_compact_limit = config.model_auto_compact_token_limit;
|
||||
|
||||
if context_window.is_some() || max_output_tokens.is_some() || auto_compact_limit.is_some() {
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(vec!["📐 ".into(), "Model Limits".bold()]));
|
||||
|
||||
if let Some(context_window) = context_window {
|
||||
let used = last_usage.tokens_in_context_window().min(context_window);
|
||||
let percent_full = if context_window > 0 {
|
||||
((used as f64 / context_window as f64) * 100.0).min(100.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
lines.push(Line::from(format!(
|
||||
" • Context window: {} used of {} ({:.0}% full)",
|
||||
format_with_separators(used),
|
||||
format_with_separators(context_window),
|
||||
percent_full
|
||||
)));
|
||||
}
|
||||
|
||||
if let Some(max_output_tokens) = max_output_tokens {
|
||||
lines.push(Line::from(format!(
|
||||
" • Max output tokens: {}",
|
||||
format_with_separators(max_output_tokens)
|
||||
)));
|
||||
}
|
||||
|
||||
if let Some(limit) = auto_compact_limit {
|
||||
if limit <= 0 {
|
||||
lines.push(Line::from(" • Auto-compact threshold: disabled"));
|
||||
} else {
|
||||
let limit_u64 = limit as u64;
|
||||
let remaining = limit_u64.saturating_sub(total_usage.total_tokens);
|
||||
lines.push(Line::from(format!(
|
||||
" • Auto-compact threshold: {} ({} remaining)",
|
||||
format_with_separators(limit_u64),
|
||||
format_with_separators(remaining)
|
||||
)));
|
||||
if total_usage.total_tokens > limit_u64 {
|
||||
lines.push(Line::from(" • Compacting will trigger on the next turn".dim()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PlainHistoryCell {
|
||||
lines,
|
||||
|
||||
Reference in New Issue
Block a user