Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/zeph-core/src/agent/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,10 @@ impl<C: Channel, T: ToolExecutor> Agent<C, T> {
let Some(msg) = self.channel.try_recv() else {
break;
};
if msg.text.trim() == "/drop-last-queued" {
self.message_queue.pop_back();
continue;
}
self.enqueue_or_merge(msg.text);
}
}
Expand Down
87 changes: 86 additions & 1 deletion crates/zeph-tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ pub struct App {
history_index: Option<usize>,
draft_input: String,
queued_count: usize,
pending_count: usize,
editing_queued: bool,
hyperlinks: Vec<HyperlinkSpan>,
cancel_signal: Option<Arc<Notify>>,
}
Expand Down Expand Up @@ -109,6 +111,8 @@ impl App {
history_index: None,
draft_input: String::new(),
queued_count: 0,
pending_count: 0,
editing_queued: false,
hyperlinks: Vec::new(),
cancel_signal: None,
}
Expand Down Expand Up @@ -240,7 +244,12 @@ impl App {

#[must_use]
pub fn queued_count(&self) -> usize {
self.queued_count
self.queued_count.max(self.pending_count)
}

#[must_use]
pub fn editing_queued(&self) -> bool {
self.editing_queued
}

#[must_use]
Expand Down Expand Up @@ -336,6 +345,7 @@ impl App {
}
}
AgentEvent::Typing => {
self.pending_count = self.pending_count.saturating_sub(1);
self.status_label = Some("thinking...".to_owned());
}
AgentEvent::Status(text) => {
Expand Down Expand Up @@ -422,6 +432,7 @@ impl App {
}
AgentEvent::QueueCount(count) => {
self.queued_count = count;
self.pending_count = count;
}
AgentEvent::DiffReady(diff) => {
if let Some(msg) = self
Expand Down Expand Up @@ -623,6 +634,24 @@ impl App {
}
}
KeyCode::Up => {
if self.input.is_empty() && self.pending_count > 0 && self.history_index.is_none() {
if let Some(last) = self.input_history.pop() {
self.input = last;
self.cursor_position = self.char_count();
self.pending_count -= 1;
self.queued_count = self.queued_count.saturating_sub(1);
self.editing_queued = true;
if let Some(pos) = self
.messages
.iter()
.rposition(|m| m.role == MessageRole::User)
{
self.messages.remove(pos);
}
let _ = self.user_input_tx.try_send("/drop-last-queued".to_owned());
}
return;
}
match self.history_index {
None => {
if self.input_history.is_empty() {
Expand Down Expand Up @@ -702,6 +731,8 @@ impl App {
self.input.clear();
self.cursor_position = 0;
self.scroll_offset = 0;
self.editing_queued = false;
self.pending_count += 1;

// Non-blocking send; if channel full, message is dropped
let _ = self.user_input_tx.try_send(text);
Expand Down Expand Up @@ -1366,4 +1397,58 @@ mod tests {
app.handle_event(AppEvent::Key(key)).unwrap();
// No way to assert "not notified" directly, but we verify no panic
}

#[test]
fn up_with_empty_input_and_queued_recalls_from_history() {
let (mut app, mut rx, _tx) = make_app();
app.input_mode = InputMode::Insert;
app.pending_count = 2;
app.input_history.push("queued msg".into());
app.messages.push(ChatMessage {
role: MessageRole::User,
content: "queued msg".into(),
streaming: false,
tool_name: None,
diff_data: None,
filter_stats: None,
});

let key = KeyEvent::new(KeyCode::Up, KeyModifiers::NONE);
app.handle_event(AppEvent::Key(key)).unwrap();

assert_eq!(app.input(), "queued msg");
assert_eq!(app.cursor_position(), 10);
assert!(app.editing_queued());
assert_eq!(app.queued_count(), 1);
assert!(app.input_history.is_empty());
assert!(app.messages().is_empty());
let sent = rx.try_recv().unwrap();
assert_eq!(sent, "/drop-last-queued");
}

#[test]
fn up_with_non_empty_input_navigates_history() {
let (mut app, mut rx, _tx) = make_app();
app.input_mode = InputMode::Insert;
app.pending_count = 2;
app.input = "hello".into();
app.cursor_position = 5;
app.input_history.push("prev".into());

let key = KeyEvent::new(KeyCode::Up, KeyModifiers::NONE);
app.handle_event(AppEvent::Key(key)).unwrap();

assert!(rx.try_recv().is_err());
assert_eq!(app.input(), "prev");
}

#[test]
fn submit_input_resets_editing_queued() {
let (mut app, _rx, _tx) = make_app();
app.editing_queued = true;
app.input = "some text".into();
app.cursor_position = 9;
app.submit_input();
assert!(!app.editing_queued());
}
}
4 changes: 4 additions & 0 deletions crates/zeph-tui/src/widgets/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ pub fn render(app: &App, frame: &mut Frame, area: Rect) {
block = block.title_bottom(Span::styled(badge, theme.highlight));
}

if app.editing_queued() {
block = block.title_bottom(Span::styled(" [editing queued] ", theme.highlight));
}

let paragraph = Paragraph::new(app.input())
.block(block)
.style(theme.input_cursor)
Expand Down
Loading