mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Simplify agent command center hints and back navigation (#46840)
## Why The command center's shortcut footer consumes several rows in narrow terminals, leaving less space for tasks. The overview also ignores attempts to dismiss it with `Esc`. ## What changed - Keep primary actions in a single-line footer and show task and navigation shortcuts in a `?` help view. - Respect configured bindings, suppress conflicting hints, and ignore key repeats when toggling help. - Let the configured cancel key close help or cancel search and rename before dismissing the overview. - Track overview dismissal while disconnected and clear stale connection notices after reconnecting. - Use picker defaults for task action and error dialogs, and the theme accent color for agent status titles. ## Testing Add coverage for help rendering, cancel navigation, custom shortcut conflicts, and overview dismissal during reconnection. Update snapshots for the compact footer and narrow layouts. GitOrigin-RevId: 457b1e667dd87be2e8216936c5402eb6ab70fde1
This commit is contained in:
@@ -965,6 +965,13 @@ impl App {
|
||||
{
|
||||
if self.reconnect.presentation == reconnect::ReconnectPresentation::Overview {
|
||||
self.chat_widget.handle_disconnected_view_key(*key);
|
||||
if self
|
||||
.chat_widget
|
||||
.selected_index_for_present_view(agents_overview::AGENTS_OVERVIEW_VIEW_ID)
|
||||
.is_none()
|
||||
{
|
||||
self.reconnect.presentation = reconnect::ReconnectPresentation::Conversation;
|
||||
}
|
||||
} else {
|
||||
self.chat_widget
|
||||
.handle_restricted_key(*key, RestrictedInputMode::Disconnected);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Status-tab hints reflect shortcuts that are not claimed by configured actions.
|
||||
//! A compact footer advertises primary actions; help lists the configured task shortcuts.
|
||||
|
||||
use super::*;
|
||||
use crate::key_hint;
|
||||
@@ -20,6 +20,23 @@ pub(super) fn hint_line(items: &[(String, String)]) -> Line<'static> {
|
||||
}
|
||||
|
||||
impl AgentsOverviewView {
|
||||
fn center_list_hint(&self, action: ListAction) -> Option<ShortcutHint> {
|
||||
self.keymap.primary_hint(action).filter(|hint| {
|
||||
!matches!(hint, ShortcutHint::Single(binding) if [
|
||||
&self.agents_keymap.resume,
|
||||
&self.agents_keymap.search,
|
||||
&self.agents_keymap.new_task,
|
||||
&self.agents_keymap.new_worktree,
|
||||
&self.agents_keymap.rename,
|
||||
&self.agents_keymap.stop,
|
||||
&self.agents_keymap.archive,
|
||||
&self.agents_keymap.delete,
|
||||
&self.agents_keymap.hide,
|
||||
&self.agents_keymap.toggle_grouping,
|
||||
].into_iter().any(|bindings| bindings.contains(binding)))
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn center_filter_hint(&self) -> String {
|
||||
[key_hint::plain(KeyCode::Tab), key_hint::shift(KeyCode::Tab)]
|
||||
.into_iter()
|
||||
@@ -33,4 +50,108 @@ impl AgentsOverviewView {
|
||||
.collect::<Vec<_>>()
|
||||
.join("/")
|
||||
}
|
||||
|
||||
pub(super) fn center_task_hints(&self) -> Vec<(String, String)> {
|
||||
let mut hints = vec![(self.center_filter_hint(), "filter".into())];
|
||||
for (action, bindings, label) in [
|
||||
("new_task", &self.agents_keymap.new_task, "new"),
|
||||
(
|
||||
"new_worktree",
|
||||
&self.agents_keymap.new_worktree,
|
||||
"new worktree",
|
||||
),
|
||||
("resume", &self.agents_keymap.resume, "resume"),
|
||||
("search", &self.agents_keymap.search, "search"),
|
||||
(
|
||||
"toggle_grouping",
|
||||
&self.agents_keymap.toggle_grouping,
|
||||
"group",
|
||||
),
|
||||
("rename", &self.agents_keymap.rename, "rename"),
|
||||
("stop", &self.agents_keymap.stop, "stop"),
|
||||
("archive", &self.agents_keymap.archive, "archive"),
|
||||
("hide", &self.agents_keymap.hide, "hide"),
|
||||
("delete", &self.agents_keymap.delete, "delete"),
|
||||
] {
|
||||
if (action != "new_worktree" || self.worktrees_enabled)
|
||||
&& let Some(hint) = self.agents_keymap.primary_hint(action, bindings)
|
||||
{
|
||||
hints.push((hint.display_label(), label.into()));
|
||||
}
|
||||
}
|
||||
for (action, label) in [
|
||||
(ListAction::MoveUp, "up"),
|
||||
(ListAction::MoveDown, "down"),
|
||||
(ListAction::Accept, "open"),
|
||||
(ListAction::PageUp, "page up"),
|
||||
(ListAction::PageDown, "page down"),
|
||||
] {
|
||||
if let Some(hint) = self.center_list_hint(action) {
|
||||
hints.push((hint.display_label(), label.into()));
|
||||
}
|
||||
}
|
||||
hints.push((
|
||||
key_hint::ctrl(KeyCode::Char('c')).display_label(),
|
||||
"quit".into(),
|
||||
));
|
||||
hints
|
||||
}
|
||||
|
||||
pub(super) fn center_footer_hints(&self) -> Vec<(String, String)> {
|
||||
let state = self.state();
|
||||
if let Some(items) = &state.key_chord_hint {
|
||||
return items.clone();
|
||||
}
|
||||
let mut hints = Vec::new();
|
||||
if !state.help
|
||||
&& !state.editing_metadata()
|
||||
&& ![KeyModifiers::NONE, KeyModifiers::SHIFT]
|
||||
.into_iter()
|
||||
.any(|modifiers| {
|
||||
self.center_shortcut_keys
|
||||
.is_pressed(KeyEvent::new(KeyCode::Char('?'), modifiers))
|
||||
})
|
||||
{
|
||||
hints.push(("?".into(), "help".into()));
|
||||
}
|
||||
let cancel = if state.help {
|
||||
self.keymap.primary_hint(ListAction::Cancel)
|
||||
} else {
|
||||
self.center_list_hint(ListAction::Cancel)
|
||||
};
|
||||
if let Some(hint) = cancel {
|
||||
hints.push((hint.display_label(), "back".into()));
|
||||
}
|
||||
if state.help {
|
||||
return hints;
|
||||
}
|
||||
if let Some(hint) = self.center_list_hint(ListAction::Accept) {
|
||||
hints.push((
|
||||
hint.display_label(),
|
||||
if state.rename_target.is_some() {
|
||||
"rename"
|
||||
} else {
|
||||
"open"
|
||||
}
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
if !state.editing_metadata() {
|
||||
for (action, bindings, label) in [
|
||||
("new_task", &self.agents_keymap.new_task, "new"),
|
||||
(
|
||||
"new_worktree",
|
||||
&self.agents_keymap.new_worktree,
|
||||
"new worktree",
|
||||
),
|
||||
] {
|
||||
if (action != "new_worktree" || self.worktrees_enabled)
|
||||
&& let Some(hint) = self.agents_keymap.primary_hint(action, bindings)
|
||||
{
|
||||
hints.push((hint.display_label(), label.into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
hints
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Status-tab shortcuts yield to configured task and list bindings.
|
||||
//! Tab filters and shortcut help yield to configured task and list bindings.
|
||||
|
||||
use super::*;
|
||||
use crossterm::event::KeyEventKind;
|
||||
|
||||
impl AgentsOverviewView {
|
||||
pub(in crate::app::agents_overview_view) fn command_center_key(
|
||||
@@ -8,12 +9,40 @@ impl AgentsOverviewView {
|
||||
key: KeyEvent,
|
||||
) -> bool {
|
||||
if self.state().editing_metadata() {
|
||||
if self.keymap.action_for(key) == Some(ListAction::Cancel) {
|
||||
self.on_ctrl_c();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
let mut state = self.state();
|
||||
if state.help && self.keymap.action_for(key) == Some(ListAction::Cancel) {
|
||||
state.help = false;
|
||||
return true;
|
||||
}
|
||||
if state.help {
|
||||
if key.code == KeyCode::Char('?')
|
||||
&& key.kind == KeyEventKind::Press
|
||||
&& !key
|
||||
.modifiers
|
||||
.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT)
|
||||
{
|
||||
state.help = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if self.center_shortcut_keys.is_pressed(key) {
|
||||
return false;
|
||||
}
|
||||
if key.code == KeyCode::Char('?')
|
||||
&& key.kind == KeyEventKind::Press
|
||||
&& !key
|
||||
.modifiers
|
||||
.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT)
|
||||
{
|
||||
state.help = !state.help;
|
||||
return true;
|
||||
}
|
||||
if key.code == KeyCode::Tab
|
||||
&& !key
|
||||
.modifiers
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Task rows, metadata editing and read-only details share a wide/narrow layout.
|
||||
//! Status tabs, task rows and read-only details share a simple wide/narrow layout.
|
||||
|
||||
use super::hints::hint_line;
|
||||
use super::*;
|
||||
@@ -48,9 +48,8 @@ struct CenterLayout {
|
||||
|
||||
impl AgentsOverviewView {
|
||||
fn center_layout(&self, area: Rect) -> CenterLayout {
|
||||
let footer_height = (self.footer_lines(area.width.saturating_sub(4)).len() as u16)
|
||||
.min(area.height.saturating_sub(4));
|
||||
let state = self.state();
|
||||
let footer_height = u16::from(area.height >= 2);
|
||||
let header_height = if area.height >= 6 { 3 } else { 0 };
|
||||
let footer = row(area, area.height - footer_height, footer_height);
|
||||
let header = row(area, /*offset*/ 0, header_height);
|
||||
@@ -123,7 +122,7 @@ impl Renderable for AgentsOverviewView {
|
||||
let layout = self.center_layout(area);
|
||||
let inset =
|
||||
|area: Rect| area.inner(Margin::new(/*horizontal*/ 2, /*vertical*/ 0));
|
||||
let footer_lines = self.footer_lines(area.width.saturating_sub(4));
|
||||
let footer_hints = self.center_footer_hints();
|
||||
let search_key = self
|
||||
.agents_keymap
|
||||
.primary_hint("search", &self.agents_keymap.search)
|
||||
@@ -223,12 +222,31 @@ impl Renderable for AgentsOverviewView {
|
||||
(state.refresh_failed && !state.loading).then(|| "Error loading tasks".to_owned())
|
||||
})
|
||||
.or_else(|| state.server_version_notice.clone());
|
||||
if let Some(notice) = notice.filter(|_| state.key_chord_hint.is_none()) {
|
||||
if let Some(notice) = notice.filter(|_| !state.help && state.key_chord_hint.is_none()) {
|
||||
line(notice.dim(), inset(layout.footer), buf);
|
||||
} else {
|
||||
Paragraph::new(footer_lines).render(inset(layout.footer), buf);
|
||||
line(hint_line(&footer_hints), inset(layout.footer), buf);
|
||||
}
|
||||
let help = state.help;
|
||||
drop(state);
|
||||
if help {
|
||||
let body = row(
|
||||
area,
|
||||
layout.header.height,
|
||||
layout.footer.y - layout.header.bottom(),
|
||||
);
|
||||
Clear.render(inset(body), buf);
|
||||
let lines = crate::wrapping::word_wrap_lines(
|
||||
vec![
|
||||
"Task shortcuts".bold().into(),
|
||||
Line::default(),
|
||||
hint_line(&self.center_task_hints()),
|
||||
],
|
||||
usize::from(inset(body).width),
|
||||
);
|
||||
Paragraph::new(lines).render(inset(body), buf);
|
||||
return;
|
||||
}
|
||||
for y in layout.gap.y..layout.gap.bottom() {
|
||||
if layout.gap.width > 1 {
|
||||
buf[(layout.gap.x + 1, y)]
|
||||
|
||||
@@ -235,7 +235,8 @@ async fn live_center_rename_retains_target_when_status_leaves_filter() -> Result
|
||||
|
||||
#[tokio::test]
|
||||
async fn live_center_navigation_and_complete_hints() {
|
||||
let app = make_test_app().await;
|
||||
let mut app = make_test_app().await;
|
||||
app.config.features.enable(Feature::Worktrees).unwrap();
|
||||
let ready = ThreadId::from_u128(/*value*/ 42);
|
||||
let needs_you = ThreadId::from_u128(/*value*/ 43);
|
||||
let unloaded = ThreadId::from_u128(/*value*/ 44);
|
||||
@@ -273,17 +274,105 @@ async fn live_center_navigation_and_complete_hints() {
|
||||
screen(&view, /*width*/ 40, /*height*/ 12)
|
||||
);
|
||||
view.handle_key_event(KeyCode::Tab.into());
|
||||
|
||||
let repeat_help =
|
||||
KeyEvent::new_with_kind(KeyCode::Char('?'), KeyModifiers::NONE, KeyEventKind::Repeat);
|
||||
view.handle_key_event(KeyCode::Char('?').into());
|
||||
view.handle_key_event(repeat_help);
|
||||
insta::assert_snapshot!(screen(&view, /*width*/ 40, /*height*/ 24));
|
||||
view.handle_key_event(KeyCode::Esc.into());
|
||||
view.handle_key_event(repeat_help);
|
||||
assert!(screen(&view, /*width*/ 40, /*height*/ 24).contains("Search tasks"));
|
||||
assert!(!view.is_complete());
|
||||
view.handle_key_event(KeyCode::Esc.into());
|
||||
assert!(view.is_complete());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn live_center_fixed_shortcuts_yield_to_configured_actions() {
|
||||
let mut app = make_test_app().await;
|
||||
let config: TuiKeymap = toml::from_str("[agents]\nresume = 'tab'").unwrap();
|
||||
let config: TuiKeymap = toml::from_str("[list]\ncancel = 'q'").unwrap();
|
||||
app.keymap = RuntimeKeymap::from_config(&config).unwrap();
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
app.app_event_tx = AppEventSender::new(tx);
|
||||
let mut view = app.agents_overview_view(Vec::new(), /*selected_thread_id*/ None);
|
||||
view.handle_key_event(KeyCode::Tab.into());
|
||||
assert!(matches!(rx.try_recv(), Ok(AppEvent::OpenResumePicker)));
|
||||
assert!(rx.try_recv().is_err());
|
||||
for editor_key in ['f', 'r'] {
|
||||
let mut view = app.agents_overview_view(
|
||||
vec![overview_thread(
|
||||
ThreadId::from_u128(/*value*/ 42),
|
||||
/*parent_thread_id*/ None,
|
||||
"Task",
|
||||
ThreadStatus::Idle,
|
||||
)],
|
||||
/*selected_thread_id*/ None,
|
||||
);
|
||||
view.handle_key_event(KeyCode::Char(editor_key).into());
|
||||
assert!(
|
||||
app.agents_overview
|
||||
.view_state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.editing_metadata()
|
||||
);
|
||||
view.handle_key_event(KeyCode::Char('q').into());
|
||||
assert!(
|
||||
!app.agents_overview
|
||||
.view_state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.editing_metadata()
|
||||
);
|
||||
assert!(!view.is_complete());
|
||||
}
|
||||
for (binding, key) in [
|
||||
("tab", KeyEvent::from(KeyCode::Tab)),
|
||||
("?", KeyEvent::from(KeyCode::Char('?'))),
|
||||
(
|
||||
"shift-?",
|
||||
KeyEvent::new(KeyCode::Char('?'), KeyModifiers::SHIFT),
|
||||
),
|
||||
("f9", KeyEvent::from(KeyCode::F(9))),
|
||||
("enter", KeyEvent::from(KeyCode::Enter)),
|
||||
] {
|
||||
let mut app = make_test_app().await;
|
||||
let config: TuiKeymap = toml::from_str(&format!(
|
||||
"[list]\ncancel = 'f9'\n[agents]\nresume = '{binding}'"
|
||||
))
|
||||
.unwrap();
|
||||
app.keymap = RuntimeKeymap::from_config(&config).unwrap();
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
app.app_event_tx = AppEventSender::new(tx);
|
||||
let mut view = app.agents_overview_view(Vec::new(), /*selected_thread_id*/ None);
|
||||
view.handle_key_event(KeyCode::Esc.into());
|
||||
assert!(!view.is_complete());
|
||||
if key.code == KeyCode::Char('?') {
|
||||
assert!(!screen(&view, /*width*/ 100, /*height*/ 24).contains("? help"));
|
||||
}
|
||||
if key.code == KeyCode::Enter {
|
||||
assert!(!screen(&view, /*width*/ 100, /*height*/ 24).contains("enter open"));
|
||||
view.handle_key_event(KeyCode::Char('?').into());
|
||||
assert!(!screen(&view, /*width*/ 100, /*height*/ 24).contains("enter open"));
|
||||
app.agents_overview
|
||||
.view_state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.server_version_notice = Some("Version notice".into());
|
||||
assert!(screen(&view, /*width*/ 100, /*height*/ 24).contains("f9 back"));
|
||||
view.handle_key_event(KeyCode::Esc.into());
|
||||
assert!(screen(&view, /*width*/ 100, /*height*/ 24).contains("Task shortcuts"));
|
||||
view.handle_key_event(KeyCode::F(9).into());
|
||||
assert!(!screen(&view, /*width*/ 100, /*height*/ 24).contains("Task shortcuts"));
|
||||
app.agents_overview
|
||||
.view_state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.server_version_notice = None;
|
||||
view.handle_key_event(KeyCode::Char('f').into());
|
||||
assert!(!screen(&view, /*width*/ 100, /*height*/ 24).contains("enter open"));
|
||||
}
|
||||
view.handle_key_event(key);
|
||||
assert!(!view.is_complete());
|
||||
assert!(
|
||||
matches!(rx.try_recv(), Ok(AppEvent::OpenResumePicker)),
|
||||
"{binding}"
|
||||
);
|
||||
assert!(rx.try_recv().is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use super::ThreadBufferedEvent;
|
||||
use super::ThreadEventStore;
|
||||
use crate::history_cell::HistoryCell;
|
||||
use crate::history_cell::plain_lines;
|
||||
use crate::style::accent_color;
|
||||
use crate::text_formatting::truncate_text;
|
||||
use codex_app_server_protocol::CollabAgentTool;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
@@ -112,7 +113,11 @@ impl AgentStatusThreadPreview {
|
||||
}
|
||||
|
||||
fn title_line(&self) -> Line<'static> {
|
||||
vec![" • ".dim(), format!("`{}`", self.agent_path).cyan()].into()
|
||||
vec![
|
||||
" • ".dim(),
|
||||
format!("`{}`", self.agent_path).fg(accent_color()),
|
||||
]
|
||||
.into()
|
||||
}
|
||||
|
||||
fn preview_lines(&self, width: u16) -> Vec<Line<'static>> {
|
||||
|
||||
@@ -157,7 +157,7 @@ impl App {
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
..SelectionViewParams::picker()
|
||||
});
|
||||
}
|
||||
|
||||
@@ -313,7 +313,7 @@ impl App {
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
..SelectionViewParams::picker()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -742,98 +742,3 @@ async fn lifecycle_removes_background_and_current_tasks_without_losing_the_dashb
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn disabled_footer_shortcuts_stay_bold_when_wrapped() {
|
||||
let app = make_test_app().await;
|
||||
let mut view = app.agents_overview_view(Vec::new(), /*selected_thread_id*/ None);
|
||||
view.handle_key_event(KeyCode::Esc.into());
|
||||
let area = Rect::new(
|
||||
/*x*/ 0, /*y*/ 0, /*width*/ 84, /*height*/ 24,
|
||||
);
|
||||
let mut buffer = ratatui::buffer::Buffer::empty(area);
|
||||
view.render(area, &mut buffer);
|
||||
let delete_key = crate::key_hint::plain(KeyCode::Delete).display_label();
|
||||
for (key, label) in [
|
||||
("x", "x stop"),
|
||||
("h", "h hide"),
|
||||
("a", "a archive"),
|
||||
(delete_key.as_str(), delete_key.as_str()),
|
||||
] {
|
||||
let cells = buffer
|
||||
.content()
|
||||
.windows(label.len())
|
||||
.find(|cells| {
|
||||
cells
|
||||
.iter()
|
||||
.map(ratatui::buffer::Cell::symbol)
|
||||
.collect::<String>()
|
||||
== label
|
||||
})
|
||||
.expect("footer shortcut");
|
||||
assert_eq!(
|
||||
cells[..key.len()]
|
||||
.iter()
|
||||
.map(|cell| cell.modifier)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![ratatui::style::Modifier::BOLD | ratatui::style::Modifier::DIM; key.len()]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn lifecycle_footer_keeps_custom_chords_with_labels() {
|
||||
let mut app = make_test_app().await;
|
||||
app.keymap = RuntimeKeymap::from_config(
|
||||
&serde_json::from_value(serde_json::json!({
|
||||
"agents": { "archive": "f5 f6", "delete": "f5 f7", "hide": "f5 f8" }
|
||||
}))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut view = app.agents_overview_view(
|
||||
vec![overview_thread(
|
||||
ThreadId::new(),
|
||||
/*parent_thread_id*/ None,
|
||||
"Task",
|
||||
ThreadStatus::Idle,
|
||||
)],
|
||||
/*selected_thread_id*/ None,
|
||||
);
|
||||
view.handle_key_event(KeyCode::Esc.into());
|
||||
for width in [36, 48, 80] {
|
||||
let area = Rect::new(/*x*/ 0, /*y*/ 0, width + 4, /*height*/ 24);
|
||||
let mut buffer = ratatui::buffer::Buffer::empty(area);
|
||||
view.render(area, &mut buffer);
|
||||
let lines = buffer
|
||||
.content()
|
||||
.chunks(usize::from(area.width))
|
||||
.map(|row| {
|
||||
row.iter()
|
||||
.map(ratatui::buffer::Cell::symbol)
|
||||
.collect::<String>()
|
||||
.trim()
|
||||
.to_string()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for label in ["archive", "delete", "hide"] {
|
||||
assert!(
|
||||
lines
|
||||
.iter()
|
||||
.any(|line| line.contains(label) && line.contains("f5"))
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
lines
|
||||
.iter()
|
||||
.all(|line| unicode_width::UnicodeWidthStr::width(line.as_str())
|
||||
<= usize::from(width))
|
||||
);
|
||||
}
|
||||
app.chat_widget.show_bottom_pane_view(Box::new(view));
|
||||
insta::assert_snapshot!(
|
||||
"agents_custom_lifecycle_chords",
|
||||
render_bottom_popup(&app.chat_widget, /*width*/ 48)
|
||||
.replace(&test_path_display("/tmp/project"), "/tmp/project")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ impl App {
|
||||
Line::from(message.clone().dim()),
|
||||
])),
|
||||
items,
|
||||
..Default::default()
|
||||
..SelectionViewParams::picker()
|
||||
});
|
||||
}
|
||||
self.chat_widget.add_error_message(message);
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
//! Layout rendering and cursor placement for the agent dashboard.
|
||||
//! Only search and rename reserve an editor row; list actions share a wrapped footer.
|
||||
|
||||
use super::*;
|
||||
|
||||
impl AgentsOverviewView {
|
||||
pub(super) fn footer_lines(&self, width: u16) -> Vec<Line<'static>> {
|
||||
let state = self.state();
|
||||
let message = if let Some(items) = &state.key_chord_hint {
|
||||
Some(
|
||||
items
|
||||
.iter()
|
||||
.map(|(key, label)| format!("{key} {label}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" "),
|
||||
)
|
||||
} else if state.editing_metadata() && state.connection_notice.is_some() {
|
||||
Some("esc cancel · actions paused until reconnected".into())
|
||||
} else if state.editing_metadata() {
|
||||
Some(format!(
|
||||
"{} {} esc cancel",
|
||||
self.keymap
|
||||
.primary_hint(ListAction::Accept)
|
||||
.map(crate::key_hint::ShortcutHint::display_label)
|
||||
.unwrap_or_default(),
|
||||
if state.rename_target.is_some() {
|
||||
"rename"
|
||||
} else {
|
||||
"open"
|
||||
}
|
||||
))
|
||||
} else if state.connection_notice.is_some() {
|
||||
Some("ctrl+c quit · actions paused until the list is refreshed".into())
|
||||
} else if state.creating_worktree {
|
||||
Some("Creating worktree… ctrl+c quit".into())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
drop(state);
|
||||
if let Some(message) = message {
|
||||
return textwrap::wrap(&message, usize::from(width.max(1)))
|
||||
.into_iter()
|
||||
.map(|line| line.into_owned().dim().into())
|
||||
.collect();
|
||||
}
|
||||
if width < 24 {
|
||||
let hints = [
|
||||
("new_task", &self.agents_keymap.new_task, "new"),
|
||||
(
|
||||
"new_worktree",
|
||||
&self.agents_keymap.new_worktree,
|
||||
"new worktree",
|
||||
),
|
||||
("search", &self.agents_keymap.search, "search"),
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|(action, _, _)| *action != "new_worktree" || self.worktrees_enabled)
|
||||
.filter_map(|(action, bindings, label)| {
|
||||
self.agents_keymap
|
||||
.primary_hint(action, bindings)
|
||||
.map(|hint| format!("{} {label}", hint.display_label()))
|
||||
});
|
||||
return hints
|
||||
.chain(["ctrl+c quit".into()])
|
||||
.flat_map(|hint| {
|
||||
textwrap::wrap(&hint, usize::from(width.max(1)))
|
||||
.into_iter()
|
||||
.map(|line| line.into_owned().dim().into())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
let list_hint = |action| {
|
||||
self.keymap.primary_hint(action).filter(|hint| {
|
||||
!matches!(hint, ShortcutHint::Single(binding)
|
||||
if [
|
||||
&self.agents_keymap.resume,
|
||||
&self.agents_keymap.search,
|
||||
&self.agents_keymap.new_task,
|
||||
&self.agents_keymap.new_worktree,
|
||||
&self.agents_keymap.rename,
|
||||
&self.agents_keymap.stop,
|
||||
&self.agents_keymap.archive,
|
||||
&self.agents_keymap.delete,
|
||||
&self.agents_keymap.hide,
|
||||
&self.agents_keymap.toggle_grouping,
|
||||
]
|
||||
.into_iter()
|
||||
.any(|bindings| bindings.contains(binding)))
|
||||
})
|
||||
};
|
||||
let navigation_hints = [ListAction::MoveUp, ListAction::MoveDown]
|
||||
.into_iter()
|
||||
.filter_map(list_hint)
|
||||
.map(|hint| hint.display_label().replace(" + ", "+"))
|
||||
.collect::<Vec<_>>();
|
||||
let navigation_hint = navigation_hints.join(
|
||||
if navigation_hints
|
||||
.iter()
|
||||
.all(|hint| hint.chars().count() == 1)
|
||||
{
|
||||
""
|
||||
} else {
|
||||
" "
|
||||
},
|
||||
);
|
||||
let mut hints: Vec<Line<'static>> = Vec::new();
|
||||
if !navigation_hint.is_empty() {
|
||||
hints.push(vec![navigation_hint.bold(), " navigate".dim()].into());
|
||||
}
|
||||
let mut add_hint = |hint: Option<ShortcutHint>, label: &'static str, enabled: bool| {
|
||||
if let Some(hint) = hint {
|
||||
let key = hint.display_label().replace(" + ", "+");
|
||||
hints.push(
|
||||
vec![
|
||||
if enabled {
|
||||
key.bold()
|
||||
} else {
|
||||
key.bold().dim()
|
||||
},
|
||||
format!(" {label}").dim(),
|
||||
]
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
};
|
||||
add_hint(
|
||||
self.agents_keymap
|
||||
.primary_hint("resume", &self.agents_keymap.resume),
|
||||
"resume",
|
||||
true,
|
||||
);
|
||||
let open_hint = list_hint(ListAction::Accept);
|
||||
add_hint(open_hint, "open", true);
|
||||
add_hint(
|
||||
self.agents_keymap
|
||||
.primary_hint("new_task", &self.agents_keymap.new_task),
|
||||
"new",
|
||||
true,
|
||||
);
|
||||
add_hint(
|
||||
self.agents_keymap
|
||||
.primary_hint("new_worktree", &self.agents_keymap.new_worktree),
|
||||
"new worktree",
|
||||
self.worktrees_enabled,
|
||||
);
|
||||
add_hint(
|
||||
self.agents_keymap
|
||||
.primary_hint("search", &self.agents_keymap.search),
|
||||
"search",
|
||||
true,
|
||||
);
|
||||
add_hint(
|
||||
self.agents_keymap
|
||||
.primary_hint("toggle_grouping", &self.agents_keymap.toggle_grouping),
|
||||
match self.state().grouping {
|
||||
AgentsOverviewGrouping::Project => "group: project",
|
||||
AgentsOverviewGrouping::Status => "group: status",
|
||||
AgentsOverviewGrouping::Model => "group: model",
|
||||
},
|
||||
true,
|
||||
);
|
||||
add_hint(
|
||||
self.agents_keymap
|
||||
.primary_hint("rename", &self.agents_keymap.rename),
|
||||
"rename",
|
||||
true,
|
||||
);
|
||||
add_hint(
|
||||
self.agents_keymap
|
||||
.primary_hint("stop", &self.agents_keymap.stop),
|
||||
"stop",
|
||||
self.selected_row()
|
||||
.is_some_and(|row| matches!(row.thread.status, ThreadStatus::Active { .. })),
|
||||
);
|
||||
for (action, bindings) in [
|
||||
("hide", &self.agents_keymap.hide),
|
||||
("archive", &self.agents_keymap.archive),
|
||||
("delete", &self.agents_keymap.delete),
|
||||
] {
|
||||
add_hint(
|
||||
self.agents_keymap.primary_hint(action, bindings),
|
||||
action,
|
||||
self.selected_row().is_some(),
|
||||
);
|
||||
}
|
||||
if self.state().editing_metadata() {
|
||||
add_hint(list_hint(ListAction::Cancel), "cancel", true);
|
||||
}
|
||||
hints.push(vec!["ctrl+c".bold(), " quit".dim()].into());
|
||||
let separator = if hints.iter().map(Line::width).sum::<usize>()
|
||||
+ hints.len().saturating_sub(1) * 2
|
||||
<= usize::from(width)
|
||||
{
|
||||
" "
|
||||
} else {
|
||||
" "
|
||||
};
|
||||
crate::footer_hint::wrap_hint_rows(hints, width, separator.len(), Line::width)
|
||||
.into_iter()
|
||||
.flat_map(|row| {
|
||||
let mut line = Line::default();
|
||||
for hint in row {
|
||||
if !line.spans.is_empty() {
|
||||
line.spans.push(separator.dim());
|
||||
}
|
||||
line.spans.extend(hint.spans);
|
||||
}
|
||||
// An individual custom chord may be wider than the entire terminal.
|
||||
crate::wrapping::word_wrap_lines([line], usize::from(width.max(1)))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -79,12 +79,7 @@ async fn older_server_notice_is_visible_in_agents_overview() {
|
||||
let view = app.agents_overview_view(Vec::new(), /*selected_thread_id*/ None);
|
||||
app.chat_widget.show_bottom_pane_view(Box::new(view));
|
||||
let rendered = render_bottom_popup(&app.chat_widget, /*width*/ 80);
|
||||
insta::assert_snapshot!(
|
||||
rendered
|
||||
.lines()
|
||||
.find(|line| line.contains("Service v0.153.0") || line.contains("Service v0.152.1"))
|
||||
.unwrap()
|
||||
);
|
||||
insta::assert_snapshot!(rendered.lines().last().unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -95,7 +90,7 @@ async fn server_version_overview_notice_updates_and_clears() {
|
||||
let view = app.agents_overview_view(Vec::new(), /*selected_thread_id*/ None);
|
||||
app.chat_widget.show_bottom_pane_view(Box::new(view));
|
||||
let rendered = render_bottom_popup(&app.chat_widget, /*width*/ 80);
|
||||
insta::assert_snapshot!(rendered.lines().find(|line| line.contains("Service v0.151.0")).unwrap(), @" Service v0.151.0 < Codex CLI v0.153.0");
|
||||
insta::assert_snapshot!(rendered.lines().last().unwrap(), @" Service v0.151.0 < Codex CLI v0.153.0");
|
||||
|
||||
app.update_server_version_overview_notice("0.153.0", /*server_version*/ None);
|
||||
let view = app.agents_overview_view(Vec::new(), /*selected_thread_id*/ None);
|
||||
@@ -2606,90 +2601,6 @@ fn trust_fixture_folders(app: &mut App) {
|
||||
#[path = "agents_overview_usage_tests.rs"]
|
||||
mod usage;
|
||||
|
||||
#[tokio::test]
|
||||
async fn command_center_escape_cancels_editors_and_never_closes_list() {
|
||||
for (vim, offline, empty, running) in [
|
||||
(false, false, false, false),
|
||||
(true, false, false, false),
|
||||
(false, true, false, false),
|
||||
(false, false, true, false),
|
||||
(false, false, false, true),
|
||||
] {
|
||||
let (mut app, mut rx, mut op_rx) = crate::app::tests::make_test_app_with_channels().await;
|
||||
if vim {
|
||||
app.chat_widget.toggle_vim_mode_and_notify();
|
||||
}
|
||||
if running {
|
||||
app.chat_widget.handle_server_notification(
|
||||
ServerNotification::TurnStarted(
|
||||
codex_app_server_protocol::TurnStartedNotification {
|
||||
thread_id: ThreadId::new().to_string(),
|
||||
turn: codex_app_server_protocol::Turn {
|
||||
id: "running".into(),
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: Vec::new(),
|
||||
status: codex_app_server_protocol::TurnStatus::InProgress,
|
||||
error: None,
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
},
|
||||
},
|
||||
),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
assert!(app.chat_widget.is_task_running_for_test());
|
||||
}
|
||||
while rx.try_recv().is_ok() {}
|
||||
let rows = if empty {
|
||||
Vec::new()
|
||||
} else {
|
||||
vec![overview_thread(
|
||||
ThreadId::new(),
|
||||
/*parent_thread_id*/ None,
|
||||
"Task",
|
||||
ThreadStatus::Idle,
|
||||
)]
|
||||
};
|
||||
let mut view = app.agents_overview_view(rows, /*selected_thread_id*/ None);
|
||||
if offline {
|
||||
app.agents_overview
|
||||
.view_state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.connection_notice = Some("Offline");
|
||||
}
|
||||
view.handle_key_event(KeyCode::Char('f').into());
|
||||
view.handle_paste("nwogrxfha".into());
|
||||
assert!(rx.try_recv().is_err());
|
||||
view.handle_key_event(KeyCode::Esc.into());
|
||||
if !empty {
|
||||
assert_eq!(view.selected_index(), Some(0));
|
||||
}
|
||||
assert_eq!(
|
||||
view.on_ctrl_c(),
|
||||
crate::bottom_pane::CancellationEvent::NotHandled
|
||||
);
|
||||
app.chat_widget.show_bottom_pane_view(Box::new(view));
|
||||
for _ in 0..3 {
|
||||
app.chat_widget.handle_key_event(KeyCode::Esc.into());
|
||||
}
|
||||
assert!(
|
||||
app.chat_widget
|
||||
.selected_index_for_present_view(AGENTS_OVERVIEW_VIEW_ID)
|
||||
.is_some()
|
||||
);
|
||||
assert!(rx.try_recv().is_err());
|
||||
app.chat_widget
|
||||
.handle_key_event(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL));
|
||||
assert!(op_rx.try_recv().is_err());
|
||||
assert!(matches!(
|
||||
rx.try_recv(),
|
||||
Ok(AppEvent::Exit(crate::app_event::ExitMode::ShutdownFirst))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn command_center_new_actions_use_selection_and_leave_metadata_text_alone() {
|
||||
let mut app = make_test_app().await;
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
#[path = "agent_center/mod.rs"]
|
||||
pub(super) mod command_center;
|
||||
|
||||
#[path = "agents_overview_render.rs"]
|
||||
mod render;
|
||||
|
||||
#[path = "agents_overview_grouping.rs"]
|
||||
mod grouping;
|
||||
|
||||
@@ -137,6 +134,7 @@ pub(super) struct AgentsOverviewViewState {
|
||||
scroll: usize,
|
||||
page_height: usize,
|
||||
status_filter: usize,
|
||||
help: bool,
|
||||
pub(super) input: String,
|
||||
pub(super) key_chord_hint: Option<Vec<(String, String)>>,
|
||||
pub(super) creating_worktree: bool,
|
||||
@@ -518,10 +516,6 @@ impl BottomPaneView for AgentsOverviewView {
|
||||
if self.command_center_key(key) {
|
||||
return;
|
||||
}
|
||||
if key.code == KeyCode::Esc {
|
||||
self.on_ctrl_c();
|
||||
return;
|
||||
}
|
||||
if key.code == KeyCode::Backspace
|
||||
&& self.state().editing_metadata()
|
||||
&& key.modifiers.is_empty()
|
||||
@@ -658,7 +652,9 @@ impl BottomPaneView for AgentsOverviewView {
|
||||
}
|
||||
ListAction::Accept => self.activate(),
|
||||
ListAction::Cancel => {
|
||||
self.on_ctrl_c();
|
||||
if matches!(self.on_ctrl_c(), CancellationEvent::NotHandled) {
|
||||
self.state().completion = Some(ViewCompletion::Cancelled);
|
||||
}
|
||||
}
|
||||
ListAction::PageUp | ListAction::PageDown => {
|
||||
self.page_selection(action);
|
||||
|
||||
@@ -271,6 +271,7 @@ impl App {
|
||||
.view_state
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
state.connection_notice = None;
|
||||
if state.creating_worktree {
|
||||
state.creating_worktree = false;
|
||||
self.pending_managed_worktree_creation = false;
|
||||
@@ -443,9 +444,6 @@ impl App {
|
||||
// A hidden overview performs this discovery when it is next opened.
|
||||
self.agents_overview.initialized = false;
|
||||
if self.reconnect.presentation == ReconnectPresentation::Overview {
|
||||
if let Ok(mut state) = self.agents_overview.view_state.lock() {
|
||||
state.connection_notice = None;
|
||||
}
|
||||
let threads = self
|
||||
.agents_overview
|
||||
.threads
|
||||
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
---
|
||||
source: tui/src/app/agents_overview_actions_tests.rs
|
||||
expression: "render_bottom_popup(&app.chat_widget,\n48).replace(&test_path_display(\"/tmp/project\"), \"/tmp/project\")"
|
||||
---
|
||||
Agent command center Group: Project g
|
||||
All 1 Needs you 0 Working 0 ›
|
||||
────────────────────────────────────────────
|
||||
Search tasks · f
|
||||
Tasks
|
||||
/tmp/project 1
|
||||
› ○ Task
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
↑↓ navigate o resume enter open n new
|
||||
w new worktree f search g group: project
|
||||
r rename x stop f5 f8 hide f5 f6 archive
|
||||
f5 f7 delete ctrl+c quit
|
||||
+2
-2
@@ -40,5 +40,5 @@ expression: normalize_agent_center_snapshot(&cached)
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" ↑↓ navigate o resume enter open n new w new worktree f search g group: project r rename "
|
||||
" x stop h hide a archive del delete ctrl+c quit "
|
||||
" │ "
|
||||
" ? help esc back enter open n new w new worktree "
|
||||
|
||||
+2
-2
@@ -24,5 +24,5 @@ expression: "normalize_agent_center_snapshot(render_bottom_popup(&app.chat_widge
|
||||
│ …
|
||||
│
|
||||
│
|
||||
↑↓ navigate o resume enter open n new w new worktree f search g group: project r rename
|
||||
x stop h hide a archive del delete ctrl+c quit
|
||||
│
|
||||
? help esc back enter open n new w new worktree
|
||||
|
||||
+2
-2
@@ -24,5 +24,5 @@ expression: "normalize_agent_center_snapshot(render_bottom_popup(&app.chat_widge
|
||||
│ …
|
||||
│
|
||||
│
|
||||
↑↓ navigate o resume enter open n new w new worktree f search g group: project r rename
|
||||
x stop h hide a archive del delete ctrl+c quit
|
||||
│
|
||||
? help esc back enter open n new w new worktree
|
||||
|
||||
+2
-2
@@ -24,5 +24,5 @@ expression: "render_bottom_popup(&app.chat_widget,\n100).replace(&test_path_disp
|
||||
│
|
||||
│
|
||||
│
|
||||
↑↓ navigate o resume enter open n new w new worktree f search g group: model r rename x stop
|
||||
h hide a archive del delete ctrl+c quit
|
||||
│
|
||||
? help esc back enter open n new w new worktree
|
||||
|
||||
+2
-2
@@ -24,5 +24,5 @@ expression: "age.replace_all(&rendered, \" [age]\")"
|
||||
|
||||
|
||||
|
||||
↑↓ navigate o resume enter open n new w new worktree f search
|
||||
g group: project r rename x stop h hide a archive del delete ctrl+c quit
|
||||
|
||||
? help esc back enter open n new w new worktree
|
||||
|
||||
+1
-1
@@ -23,4 +23,4 @@ expression: "screen(&view, 160, 22)"
|
||||
│
|
||||
│
|
||||
│
|
||||
↑↓ navigate o resume enter open n new w new worktree f search g group: project r rename x stop h hide a archive del delete ctrl+c quit
|
||||
? help esc back enter open n new w new worktree
|
||||
|
||||
+1
-1
@@ -19,4 +19,4 @@ expression: rendered
|
||||
|
||||
|
||||
|
||||
enter rename esc cancel
|
||||
esc back enter rename
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: tui/src/app/agent_center_tests.rs
|
||||
expression: "screen(&view, 40, 24)"
|
||||
---
|
||||
Agent command center Group: Projec…
|
||||
All 3 Needs you 1 Working 0 ›
|
||||
────────────────────────────────────
|
||||
Task shortcuts
|
||||
|
||||
tab/shift+tab filter n new w new
|
||||
worktree o resume f search g
|
||||
group r rename x stop a archive
|
||||
h hide del delete ↑ up ↓ down
|
||||
enter open pgup page up pgdn page
|
||||
down ctrl+c quit
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
esc back
|
||||
+6
-6
@@ -6,11 +6,11 @@ expression: "screen(&view, 40, 12)"
|
||||
‹ Working 0 Ready 1 Inactive 1
|
||||
────────────────────────────────────
|
||||
Search tasks · f
|
||||
↑ Tasks
|
||||
Tasks
|
||||
/tmp/project 1 of 3
|
||||
› ○ Unloaded task
|
||||
|
||||
↑↓ navigate o resume enter open
|
||||
n new w new worktree f search
|
||||
g group: project r rename x stop
|
||||
h hide a archive del delete
|
||||
ctrl+c quit
|
||||
|
||||
|
||||
|
||||
? help esc back enter open n new…
|
||||
|
||||
+5
@@ -20,4 +20,9 @@ expression: "render_bottom_popup(&app.chat_widget, 12)"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Service…
|
||||
|
||||
+2
-2
@@ -24,5 +24,5 @@ expression: "render_bottom_popup(&app.chat_widget,\n96).replace(&format!(\"{proj
|
||||
│ Review parser
|
||||
│
|
||||
│
|
||||
↑↓ navigate o resume enter open n new w new worktree f search g group: project r rename
|
||||
x stop h hide a archive del delete ctrl+c quit
|
||||
│
|
||||
? help esc back enter open n new w new worktree
|
||||
|
||||
@@ -215,6 +215,10 @@ async fn reconnect_restores_history_permissions_and_keeps_old_input_paused() ->
|
||||
},
|
||||
);
|
||||
}
|
||||
if edit_offline {
|
||||
let view = app.agents_overview_view(Vec::new(), /*selected_thread_id*/ None);
|
||||
app.chat_widget.show_bottom_pane_view(Box::new(view));
|
||||
}
|
||||
app.begin_reconnect();
|
||||
if deferred_notice {
|
||||
assert_eq!(
|
||||
@@ -227,6 +231,12 @@ async fn reconnect_restores_history_permissions_and_keeps_old_input_paused() ->
|
||||
);
|
||||
}
|
||||
if edit_offline {
|
||||
for key in [KeyCode::Esc, KeyCode::Enter] {
|
||||
app.handle_tui_event(&mut tui, &mut session, TuiEvent::Key(key.into()))
|
||||
.await?;
|
||||
}
|
||||
assert!(!app.chat_widget.has_active_view());
|
||||
assert_eq!(app.chat_widget.composer_text_with_pending(), "kept draft");
|
||||
app.handle_tui_event(
|
||||
&mut tui,
|
||||
&mut session,
|
||||
@@ -280,6 +290,17 @@ async fn reconnect_restores_history_permissions_and_keeps_old_input_paused() ->
|
||||
);
|
||||
app.finish_reconnect(&mut tui, &mut session, &mut events, connected, "2.1.0")
|
||||
.await?;
|
||||
if edit_offline {
|
||||
assert!(!app.chat_widget.has_active_view());
|
||||
assert!(
|
||||
app.agents_overview
|
||||
.view_state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.connection_notice
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
assert!(app.pending_server_profiles.is_empty());
|
||||
assert!(!app.pending_managed_worktree_creation);
|
||||
assert!(
|
||||
|
||||
@@ -438,20 +438,6 @@ async fn reconnect_daemon_command_center_after_socket_replacement_without_a_conv
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert!(app.chat_widget.has_active_view());
|
||||
app.handle_tui_event(
|
||||
&mut tui,
|
||||
&mut session,
|
||||
TuiEvent::Key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert!(app.chat_widget.has_active_view());
|
||||
// Esc stays in the overview. Dismiss the retained view explicitly
|
||||
// to inspect the unavailable conversation and its cached draft below.
|
||||
app.agents_overview.view_state.lock().unwrap().completion =
|
||||
Some(crate::bottom_pane::ViewCompletion::Accepted);
|
||||
app.chat_widget.handle_key_event(KeyCode::Null.into());
|
||||
assert!(!app.chat_widget.has_active_view());
|
||||
assert_eq!(app.current_displayed_thread_id(), Some(id));
|
||||
let history = drain_history(&mut app, &mut tui, &mut session, &mut events).await?;
|
||||
|
||||
+1
@@ -24,4 +24,5 @@ expression: "render_bottom_popup(&app.chat_widget, 100)"
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
Service v2.0.0 ≠ Codex CLI v0.0.0 · /daemon
|
||||
|
||||
+2
-2
@@ -24,5 +24,5 @@ expression: "render_bottom_popup(&app.chat_widget,\n96).replace(&format!(\"{proj
|
||||
│
|
||||
│
|
||||
│
|
||||
↑↓ navigate o resume enter open n new w new worktree f search g group: project r rename
|
||||
x stop h hide a archive del delete ctrl+c quit
|
||||
│
|
||||
? help esc back enter open n new w new worktree
|
||||
|
||||
+2
-2
@@ -24,5 +24,5 @@ expression: "render_bottom_popup(&app.chat_widget,\n96).replace(&format!(\"{proj
|
||||
│ Repair authentication
|
||||
│
|
||||
│
|
||||
↑↓ navigate o resume enter open n new w new worktree f search g group: project r rename
|
||||
x stop h hide a archive del delete ctrl+c quit
|
||||
│
|
||||
? help esc back enter open n new w new worktree
|
||||
|
||||
+4
-4
@@ -22,7 +22,7 @@ expression: rendered
|
||||
|
||||
|
||||
|
||||
↑↓ navigate o resume enter open n new
|
||||
w new worktree f search g group: project
|
||||
r rename x stop h hide a archive del delete
|
||||
ctrl+c quit
|
||||
|
||||
|
||||
|
||||
? help esc back enter open n new w new …
|
||||
|
||||
+2
-2
@@ -15,6 +15,7 @@ expression: "render_bottom_popup(&app.chat_widget,\n96).replace(&format!(\"{proj
|
||||
│ Found the regression in the parser.
|
||||
│ Found the regression in the parser.
|
||||
│ Found the regression in the parser.
|
||||
│ Found the regression in the parser.
|
||||
│ …
|
||||
│
|
||||
│ Project
|
||||
@@ -24,5 +25,4 @@ expression: "render_bottom_popup(&app.chat_widget,\n96).replace(&format!(\"{proj
|
||||
│ Prompt
|
||||
│ Investigate parser
|
||||
│ …
|
||||
↑↓ navigate o resume enter open n new w new worktree f search g group: project r rename
|
||||
x stop h hide a archive del delete ctrl+c quit
|
||||
? help esc back enter open n new w new worktree
|
||||
|
||||
+2
-2
@@ -24,5 +24,5 @@ expression: "render_bottom_popup(&app.chat_widget,\n96).replace(&format!(\"{proj
|
||||
│
|
||||
│
|
||||
│
|
||||
↑↓ navigate o resume enter open n new w new worktree f search g group: project r rename
|
||||
x stop h hide a archive del delete ctrl+c quit
|
||||
│
|
||||
? help esc back enter open n new w new worktree
|
||||
|
||||
+1
-1
@@ -25,4 +25,4 @@ expression: normalize_agent_center_snapshot(grouping)
|
||||
|
||||
|
||||
|
||||
↑↓ navigate o resume enter open n new w new worktree f search g group: project r rename x stop h hide a archive del delete ctrl+c quit
|
||||
? help esc back enter open n new w new worktree
|
||||
|
||||
Reference in New Issue
Block a user