mirror of
https://github.com/openai/codex.git
synced 2026-11-12 14:41:42 +00:00
Add a hide shortcut to the agents overview (#44424)
## What changed - Add `Ctrl+W` to hide the selected task without stopping it. Hidden tasks stay hidden through activity and metadata refreshes until explicitly resumed or the TUI restarts. - Expose `agents.hide` in keymap configuration and the shortcut picker, and let the default binding yield to existing custom shortcuts. - Wrap overview footer hints while keeping shortcuts and their labels together when they fit. ## Testing Add coverage for hiding filtered tasks across statuses, visibility through activity and refreshes, explicit resume, existing keybinding compatibility, and custom chord footer rendering at narrow widths. GitOrigin-RevId: 980726e1095b8d5bad8901be919decdde8c0f925
This commit is contained in:
1 parent
ddea03ad04
commit
bf5ebd98c5
24 files changed
+339
-24
No files matched your search
@@ -443,6 +443,8 @@ pub struct TuiAgentsKeymap {
|
|||||||
pub rename: Option<KeybindingsSpec>,
|
pub rename: Option<KeybindingsSpec>,
|
||||||
/// Stop the selected running task.
|
/// Stop the selected running task.
|
||||||
pub stop: Option<KeybindingsSpec>,
|
pub stop: Option<KeybindingsSpec>,
|
||||||
|
/// Hide the selected task until explicitly resumed or the TUI restarts.
|
||||||
|
pub hide: Option<KeybindingsSpec>,
|
||||||
/// Toggle grouping tasks by status or project.
|
/// Toggle grouping tasks by status or project.
|
||||||
pub toggle_grouping: Option<KeybindingsSpec>,
|
pub toggle_grouping: Option<KeybindingsSpec>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3993,6 +3993,7 @@
|
|||||||
],
|
],
|
||||||
"default": {
|
"default": {
|
||||||
"agents": {
|
"agents": {
|
||||||
|
"hide": null,
|
||||||
"new_task": null,
|
"new_task": null,
|
||||||
"rename": null,
|
"rename": null,
|
||||||
"resume": null,
|
"resume": null,
|
||||||
@@ -4303,6 +4304,14 @@
|
|||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"description": "Shortcuts specific to the shared agents overview.",
|
"description": "Shortcuts specific to the shared agents overview.",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"hide": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/KeybindingsSpec"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Hide the selected task until explicitly resumed or the TUI restarts."
|
||||||
|
},
|
||||||
"new_task": {
|
"new_task": {
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
@@ -4808,6 +4817,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"default": {
|
"default": {
|
||||||
|
"hide": null,
|
||||||
"new_task": null,
|
"new_task": null,
|
||||||
"rename": null,
|
"rename": null,
|
||||||
"resume": null,
|
"resume": null,
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ pub(crate) const AGENTS_OVERVIEW_VIEW_ID: &str = "agents-overview";
|
|||||||
pub(super) struct AgentsOverviewState {
|
pub(super) struct AgentsOverviewState {
|
||||||
/// Missing metadata records a local resume until the next metadata refresh.
|
/// Missing metadata records a local resume until the next metadata refresh.
|
||||||
pub(super) threads: HashMap<ThreadId, Option<Thread>>,
|
pub(super) threads: HashMap<ThreadId, Option<Thread>>,
|
||||||
|
/// Local visibility only; activity and metadata refreshes never reveal hidden roots.
|
||||||
|
pub(super) hidden_threads: HashSet<ThreadId>,
|
||||||
pub(super) last_messages: HashMap<ThreadId, String>,
|
pub(super) last_messages: HashMap<ThreadId, String>,
|
||||||
pub(super) activity: HashMap<ThreadId, super::agents_overview_details::AgentsOverviewActivity>,
|
pub(super) activity: HashMap<ThreadId, super::agents_overview_details::AgentsOverviewActivity>,
|
||||||
pub(super) initialized: bool,
|
pub(super) initialized: bool,
|
||||||
@@ -270,6 +272,9 @@ impl App {
|
|||||||
let Ok(thread_id) = ThreadId::from_string(&root.id) else {
|
let Ok(thread_id) = ThreadId::from_string(&root.id) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
if self.agents_overview.hidden_threads.contains(&thread_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
rows.push(AgentsOverviewRow {
|
rows.push(AgentsOverviewRow {
|
||||||
details: self.agents_overview_details(root, &children),
|
details: self.agents_overview_details(root, &children),
|
||||||
thread: root.clone(),
|
thread: root.clone(),
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn lifecycle_shortcuts_target_filtered_task_in_any_state() {
|
||||||
|
let mut app = make_test_app().await;
|
||||||
|
let mut keymap = TuiKeymap::default();
|
||||||
|
keymap.agents.hide = Some(KeybindingsSpec::One(KeybindingSpec("f7".into())));
|
||||||
|
app.keymap = RuntimeKeymap::from_config(&keymap).unwrap();
|
||||||
|
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
|
app.app_event_tx = AppEventSender::new(tx);
|
||||||
|
for status in [
|
||||||
|
ThreadStatus::Idle,
|
||||||
|
ThreadStatus::NotLoaded,
|
||||||
|
ThreadStatus::Active {
|
||||||
|
active_flags: Vec::new(),
|
||||||
|
},
|
||||||
|
] {
|
||||||
|
app.agents_overview
|
||||||
|
.view_state
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.focus_composer();
|
||||||
|
let target = ThreadId::new();
|
||||||
|
let mut view = app.agents_overview_view(
|
||||||
|
vec![
|
||||||
|
overview_thread(
|
||||||
|
ThreadId::new(),
|
||||||
|
/*parent_thread_id*/ None,
|
||||||
|
"Other",
|
||||||
|
ThreadStatus::Idle,
|
||||||
|
),
|
||||||
|
overview_thread(target, /*parent_thread_id*/ None, "Target", status),
|
||||||
|
],
|
||||||
|
Some(target),
|
||||||
|
);
|
||||||
|
view.handle_key_event(KeyCode::Esc.into());
|
||||||
|
view.handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::CONTROL));
|
||||||
|
for character in "Target".chars() {
|
||||||
|
view.handle_key_event(KeyCode::Char(character).into());
|
||||||
|
}
|
||||||
|
view.handle_key_event(KeyCode::F(7).into());
|
||||||
|
assert!(
|
||||||
|
matches!(rx.try_recv(), Ok(AppEvent::HideAgentsOverviewThread { thread_id }) if thread_id == target)
|
||||||
|
);
|
||||||
|
assert!(rx.try_recv().is_err());
|
||||||
|
view.handle_key_event(KeyCode::Esc.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn hidden_task_stays_hidden_through_activity_and_seed_until_explicit_resume() -> Result<()> {
|
||||||
|
let (mut app, mut rx, _op_rx) = crate::app::tests::make_test_app_with_channels().await;
|
||||||
|
let mut app_server = crate::start_embedded_app_server_for_picker(&app.config).await?;
|
||||||
|
let started = app_server.start_thread(&app.config).await?;
|
||||||
|
let id = started.session.thread_id;
|
||||||
|
app.enqueue_primary_thread_session(started.session, started.turns)
|
||||||
|
.await?;
|
||||||
|
let thread = overview_thread(
|
||||||
|
id,
|
||||||
|
/*parent_thread_id*/ None,
|
||||||
|
"Hidden task",
|
||||||
|
ThreadStatus::Idle,
|
||||||
|
);
|
||||||
|
app.agents_overview.threads.insert(id, Some(thread.clone()));
|
||||||
|
let mut tui = crate::tui::test_support::make_test_tui()?;
|
||||||
|
let view = app.agents_overview_view(vec![thread.clone()], Some(id));
|
||||||
|
app.chat_widget.show_bottom_pane_view(Box::new(view));
|
||||||
|
app.chat_widget.handle_key_event(KeyCode::Esc.into());
|
||||||
|
app.chat_widget
|
||||||
|
.handle_key_event(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
|
||||||
|
let hide = std::iter::from_fn(|| rx.try_recv().ok())
|
||||||
|
.find(|event| matches!(event, AppEvent::HideAgentsOverviewThread { .. }))
|
||||||
|
.expect("shortcut requests hiding the task");
|
||||||
|
Box::pin(app.handle_event(&mut tui, &mut app_server, hide)).await?;
|
||||||
|
app.track_agents_overview_notification(&ServerNotification::ThreadStarted(
|
||||||
|
ThreadStartedNotification {
|
||||||
|
thread: thread.clone(),
|
||||||
|
},
|
||||||
|
));
|
||||||
|
app.track_agents_overview_notification(&ServerNotification::ThreadStatusChanged(
|
||||||
|
codex_app_server_protocol::ThreadStatusChangedNotification {
|
||||||
|
thread_id: id.to_string(),
|
||||||
|
status: ThreadStatus::Active {
|
||||||
|
active_flags: Vec::new(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
));
|
||||||
|
app.track_agents_overview_notification(&ServerNotification::ThreadClosed(
|
||||||
|
ThreadClosedNotification {
|
||||||
|
thread_id: id.to_string(),
|
||||||
|
},
|
||||||
|
));
|
||||||
|
let request_id = Uuid::new_v4();
|
||||||
|
app.agents_overview.initialized = false;
|
||||||
|
app.agents_overview.request_id = Some(request_id);
|
||||||
|
app.apply_agents_overview_thread_refresh(
|
||||||
|
&app_server,
|
||||||
|
request_id,
|
||||||
|
Ok(AgentsOverviewThreadRefresh {
|
||||||
|
threads: HashMap::from([(id, Some(thread.clone()))]),
|
||||||
|
last_messages: HashMap::new(),
|
||||||
|
recent_seed_complete: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
app.agents_overview_view(vec![thread.clone()], /*selected_thread_id*/ None)
|
||||||
|
.thread_ids(),
|
||||||
|
Vec::<ThreadId>::new()
|
||||||
|
);
|
||||||
|
assert_eq!(app.primary_thread_id, Some(id));
|
||||||
|
app.resume_target_session(
|
||||||
|
&mut tui,
|
||||||
|
&mut app_server,
|
||||||
|
SessionTarget {
|
||||||
|
thread_id: id,
|
||||||
|
path: None,
|
||||||
|
cwd: None,
|
||||||
|
history_mode: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(
|
||||||
|
app.agents_overview_view(vec![thread], /*selected_thread_id*/ None)
|
||||||
|
.thread_ids(),
|
||||||
|
vec![id]
|
||||||
|
);
|
||||||
|
app_server.shutdown().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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": { "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<_>>();
|
||||||
|
assert!(
|
||||||
|
lines
|
||||||
|
.iter()
|
||||||
|
.any(|line| line.contains("hide") && 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")
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ impl AgentsOverviewView {
|
|||||||
&self.agents_keymap.new_task,
|
&self.agents_keymap.new_task,
|
||||||
&self.agents_keymap.rename,
|
&self.agents_keymap.rename,
|
||||||
&self.agents_keymap.stop,
|
&self.agents_keymap.stop,
|
||||||
|
&self.agents_keymap.hide,
|
||||||
&self.agents_keymap.toggle_grouping,
|
&self.agents_keymap.toggle_grouping,
|
||||||
]
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -46,15 +47,20 @@ impl AgentsOverviewView {
|
|||||||
" "
|
" "
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let mut footer_spans = Vec::new();
|
let mut hints: Vec<Line<'static>> = Vec::new();
|
||||||
if !navigation_hint.is_empty() {
|
if !navigation_hint.is_empty() {
|
||||||
footer_spans.extend([navigation_hint.bold(), " navigate ".dim()]);
|
hints.push(vec![navigation_hint.bold(), " navigate".dim()].into());
|
||||||
}
|
}
|
||||||
let mut add_hint = |hint: Option<ShortcutHint>, label: &'static str, enabled: bool| {
|
let mut add_hint = |hint: Option<ShortcutHint>, label: &'static str, enabled: bool| {
|
||||||
if let Some(hint) = hint {
|
if let Some(hint) = hint {
|
||||||
let key = hint.display_label().replace(" + ", "+");
|
let key = hint.display_label().replace(" + ", "+");
|
||||||
footer_spans.push(if enabled { key.bold() } else { key.dim() });
|
hints.push(
|
||||||
footer_spans.push(format!(" {label} ").dim());
|
vec![
|
||||||
|
if enabled { key.bold() } else { key.dim() },
|
||||||
|
format!(" {label}").dim(),
|
||||||
|
]
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
add_hint(
|
add_hint(
|
||||||
@@ -99,16 +105,35 @@ impl AgentsOverviewView {
|
|||||||
self.selected_row()
|
self.selected_row()
|
||||||
.is_some_and(|row| matches!(row.thread.status, ThreadStatus::Active { .. })),
|
.is_some_and(|row| matches!(row.thread.status, ThreadStatus::Active { .. })),
|
||||||
);
|
);
|
||||||
|
add_hint(
|
||||||
|
self.agents_keymap
|
||||||
|
.primary_hint("hide", &self.agents_keymap.hide),
|
||||||
|
"hide",
|
||||||
|
self.selected_row().is_some(),
|
||||||
|
);
|
||||||
add_hint(list_hint(ListAction::Cancel), "back", true);
|
add_hint(list_hint(ListAction::Cancel), "back", true);
|
||||||
let mut footer_line: Line = footer_spans.into();
|
let separator = if hints.iter().map(Line::width).sum::<usize>()
|
||||||
if footer_line.width() > usize::from(width) {
|
+ hints.len().saturating_sub(1) * 2
|
||||||
for span in &mut footer_line.spans {
|
<= usize::from(width)
|
||||||
if span.content.ends_with(" ") {
|
{
|
||||||
span.content.to_mut().pop();
|
" "
|
||||||
|
} 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)))
|
||||||
crate::wrapping::word_wrap_lines([footer_line], usize::from(width))
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2258,3 +2258,6 @@ async fn command_center_handles_resume_failure_and_success() -> Result<()> {
|
|||||||
server.shutdown().await?;
|
server.shutdown().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[path = "agents_overview_actions_tests.rs"]
|
||||||
|
mod actions;
|
||||||
@@ -689,6 +689,14 @@ impl BottomPaneView for AgentsOverviewView {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if self.agents_keymap.hide.is_pressed(key) {
|
||||||
|
if let Some(row) = self.selected_row() {
|
||||||
|
self.app_event_tx.send(AppEvent::HideAgentsOverviewThread {
|
||||||
|
thread_id: row.thread_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if self.agents_keymap.stop.is_pressed(key) {
|
if self.agents_keymap.stop.is_pressed(key) {
|
||||||
if let Some(row) = self.selected_row()
|
if let Some(row) = self.selected_row()
|
||||||
&& matches!(row.thread.status, ThreadStatus::Active { .. })
|
&& matches!(row.thread.status, ThreadStatus::Active { .. })
|
||||||
|
|||||||
@@ -2803,6 +2803,10 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AppEvent::HideAgentsOverviewThread { thread_id } => {
|
||||||
|
self.agents_overview.hidden_threads.insert(thread_id);
|
||||||
|
self.repaint_agents_overview();
|
||||||
|
}
|
||||||
AppEvent::StopAgentsOverviewThread { thread_id } => {
|
AppEvent::StopAgentsOverviewThread { thread_id } => {
|
||||||
self.stop_agents_overview_thread(app_server, thread_id)
|
self.stop_agents_overview_thread(app_server, thread_id)
|
||||||
.await;
|
.await;
|
||||||
|
|||||||
@@ -1191,6 +1191,10 @@ impl App {
|
|||||||
target_session: SessionTarget,
|
target_session: SessionTarget,
|
||||||
) -> Result<AppRunControl> {
|
) -> Result<AppRunControl> {
|
||||||
if self.ignore_same_thread_resume(&target_session) {
|
if self.ignore_same_thread_resume(&target_session) {
|
||||||
|
self.agents_overview
|
||||||
|
.hidden_threads
|
||||||
|
.remove(&target_session.thread_id);
|
||||||
|
self.repaint_agents_overview();
|
||||||
tui.frame_requester().schedule_frame();
|
tui.frame_requester().schedule_frame();
|
||||||
return Ok(AppRunControl::Continue);
|
return Ok(AppRunControl::Continue);
|
||||||
}
|
}
|
||||||
|
|||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: tui/src/app/agents_overview_actions_tests.rs
|
||||||
|
expression: "render_bottom_popup(&app.chat_widget, 48)"
|
||||||
|
---
|
||||||
|
Agent command center
|
||||||
|
0 need input 0 working 1 ready
|
||||||
|
────────────────────────────────────────────
|
||||||
|
/tmp/project 1
|
||||||
|
› ○ Task Ready
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
New task
|
||||||
|
|
||||||
|
› Describe a new task
|
||||||
|
|
||||||
|
↑↓ navigate ctrl+o resume → open
|
||||||
|
ctrl+n new task ctrl+f search ctrl+s group
|
||||||
|
ctrl+r rename ctrl+x stop f5 f8 hide
|
||||||
|
esc back
|
||||||
@@ -1511,6 +1511,7 @@ impl App {
|
|||||||
self.recap.reset_for_new_thread(Instant::now());
|
self.recap.reset_for_new_thread(Instant::now());
|
||||||
}
|
}
|
||||||
self.primary_thread_id = Some(thread_id);
|
self.primary_thread_id = Some(thread_id);
|
||||||
|
self.agents_overview.hidden_threads.remove(&thread_id);
|
||||||
self.agents_overview.threads.entry(thread_id).or_default();
|
self.agents_overview.threads.entry(thread_id).or_default();
|
||||||
self.primary_session_configured = Some(session.clone());
|
self.primary_session_configured = Some(session.clone());
|
||||||
self.upsert_agent_picker_thread(
|
self.upsert_agent_picker_thread(
|
||||||
|
|||||||
@@ -310,6 +310,10 @@ pub(crate) enum AppEvent {
|
|||||||
StopAgentsOverviewThread {
|
StopAgentsOverviewThread {
|
||||||
thread_id: ThreadId,
|
thread_id: ThreadId,
|
||||||
},
|
},
|
||||||
|
/// Hide a dashboard row locally without stopping its task.
|
||||||
|
HideAgentsOverviewThread {
|
||||||
|
thread_id: ThreadId,
|
||||||
|
},
|
||||||
/// Start the shared app-server daemon without moving the current embedded session.
|
/// Start the shared app-server daemon without moving the current embedded session.
|
||||||
#[cfg(any(unix, windows))]
|
#[cfg(any(unix, windows))]
|
||||||
StartAgentsDaemon,
|
StartAgentsDaemon,
|
||||||
|
|||||||
@@ -411,6 +411,7 @@ pub(crate) struct AgentsKeymap {
|
|||||||
pub(crate) new_task: Vec<KeyBinding>,
|
pub(crate) new_task: Vec<KeyBinding>,
|
||||||
pub(crate) rename: Vec<KeyBinding>,
|
pub(crate) rename: Vec<KeyBinding>,
|
||||||
pub(crate) stop: Vec<KeyBinding>,
|
pub(crate) stop: Vec<KeyBinding>,
|
||||||
|
pub(crate) hide: Vec<KeyBinding>,
|
||||||
pub(crate) toggle_grouping: Vec<KeyBinding>,
|
pub(crate) toggle_grouping: Vec<KeyBinding>,
|
||||||
chord_hints: Arc<RuntimeChordKeymap>,
|
chord_hints: Arc<RuntimeChordKeymap>,
|
||||||
}
|
}
|
||||||
@@ -1289,10 +1290,24 @@ impl RuntimeKeymap {
|
|||||||
new_task: resolve_local!(keymap, defaults, agents, new_task),
|
new_task: resolve_local!(keymap, defaults, agents, new_task),
|
||||||
rename: resolve_local!(keymap, defaults, agents, rename),
|
rename: resolve_local!(keymap, defaults, agents, rename),
|
||||||
stop: resolve_local!(keymap, defaults, agents, stop),
|
stop: resolve_local!(keymap, defaults, agents, stop),
|
||||||
|
hide: resolve_local!(keymap, defaults, agents, hide),
|
||||||
toggle_grouping: resolve_local!(keymap, defaults, agents, toggle_grouping),
|
toggle_grouping: resolve_local!(keymap, defaults, agents, toggle_grouping),
|
||||||
chord_hints: Arc::clone(&chords),
|
chord_hints: Arc::clone(&chords),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The new default yields to existing user bindings in the dashboard.
|
||||||
|
if keymap.agents.hide.is_none()
|
||||||
|
&& (configured_context_alias_is_used(&keymap.agents, "ctrl-w")
|
||||||
|
|| configured_context_alias_is_used(&keymap.list, "ctrl-w")
|
||||||
|
|| configured_context_alias_is_used(&keymap.global, "ctrl-w")
|
||||||
|
|| chords.bindings.iter().any(|chord| {
|
||||||
|
chord.action.context.overlaps(KeymapContext::Agents)
|
||||||
|
&& agents.hide.contains(&chord.chord.prefix)
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
agents.hide.clear();
|
||||||
|
}
|
||||||
|
|
||||||
let approval = ApprovalKeymap {
|
let approval = ApprovalKeymap {
|
||||||
open_fullscreen: resolve_local!(keymap, defaults, approval, open_fullscreen),
|
open_fullscreen: resolve_local!(keymap, defaults, approval, open_fullscreen),
|
||||||
open_thread: resolve_local!(keymap, defaults, approval, open_thread),
|
open_thread: resolve_local!(keymap, defaults, approval, open_thread),
|
||||||
@@ -1425,6 +1440,7 @@ impl RuntimeKeymap {
|
|||||||
(keymap.agents.new_task.as_ref(), &mut agents.new_task),
|
(keymap.agents.new_task.as_ref(), &mut agents.new_task),
|
||||||
(keymap.agents.rename.as_ref(), &mut agents.rename),
|
(keymap.agents.rename.as_ref(), &mut agents.rename),
|
||||||
(keymap.agents.stop.as_ref(), &mut agents.stop),
|
(keymap.agents.stop.as_ref(), &mut agents.stop),
|
||||||
|
(keymap.agents.hide.as_ref(), &mut agents.hide),
|
||||||
(
|
(
|
||||||
keymap.agents.toggle_grouping.as_ref(),
|
keymap.agents.toggle_grouping.as_ref(),
|
||||||
&mut agents.toggle_grouping,
|
&mut agents.toggle_grouping,
|
||||||
@@ -1789,6 +1805,7 @@ impl RuntimeKeymap {
|
|||||||
new_task: default_bindings![ctrl(KeyCode::Char('n'))],
|
new_task: default_bindings![ctrl(KeyCode::Char('n'))],
|
||||||
rename: default_bindings![ctrl(KeyCode::Char('r'))],
|
rename: default_bindings![ctrl(KeyCode::Char('r'))],
|
||||||
stop: default_bindings![ctrl(KeyCode::Char('x'))],
|
stop: default_bindings![ctrl(KeyCode::Char('x'))],
|
||||||
|
hide: default_bindings![ctrl(KeyCode::Char('w'))],
|
||||||
toggle_grouping: default_bindings![ctrl(KeyCode::Char('s'))],
|
toggle_grouping: default_bindings![ctrl(KeyCode::Char('s'))],
|
||||||
chord_hints: Arc::default(),
|
chord_hints: Arc::default(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -398,6 +398,7 @@ define_runtime_action_bindings! {
|
|||||||
new_task,
|
new_task,
|
||||||
rename,
|
rename,
|
||||||
stop,
|
stop,
|
||||||
|
hide,
|
||||||
toggle_grouping,
|
toggle_grouping,
|
||||||
],
|
],
|
||||||
"approval" => Approval, approval, approval [
|
"approval" => Approval, approval, approval [
|
||||||
|
|||||||
@@ -37,3 +37,19 @@ See the Codex keymap documentation for supported actions and examples."
|
|||||||
config.remove(context);
|
config.remove(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_agents_defaults_preserve_existing_custom_bindings() {
|
||||||
|
for context in ["agents", "list"] {
|
||||||
|
let existing = if context == "agents" {
|
||||||
|
"stop"
|
||||||
|
} else {
|
||||||
|
"move_down"
|
||||||
|
};
|
||||||
|
let keymap: TuiKeymap =
|
||||||
|
serde_json::from_value(json!({context: {existing: "ctrl-w"}})).unwrap();
|
||||||
|
let runtime =
|
||||||
|
RuntimeKeymap::from_config(&keymap).expect("existing configuration remains valid");
|
||||||
|
assert!(runtime.agents.hide.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -218,6 +218,7 @@ pub(super) const KEYMAP_ACTIONS: &[KeymapActionDescriptor] = &[
|
|||||||
action("agents", "Agents", "new_task", "Start composing a new agent task."),
|
action("agents", "Agents", "new_task", "Start composing a new agent task."),
|
||||||
action("agents", "Agents", "rename", "Rename the selected task."),
|
action("agents", "Agents", "rename", "Rename the selected task."),
|
||||||
action("agents", "Agents", "stop", "Stop the selected running task."),
|
action("agents", "Agents", "stop", "Stop the selected running task."),
|
||||||
|
action("agents", "Agents", "hide", "Hide the selected task until explicitly resumed."),
|
||||||
action("agents", "Agents", "toggle_grouping", "Group tasks by status or project."),
|
action("agents", "Agents", "toggle_grouping", "Group tasks by status or project."),
|
||||||
action("approval", "Approval", "open_fullscreen", "Open approval details fullscreen."),
|
action("approval", "Approval", "open_fullscreen", "Open approval details fullscreen."),
|
||||||
action("approval", "Approval", "open_thread", "Open the approval source thread when available."),
|
action("approval", "Approval", "open_thread", "Open the approval source thread when available."),
|
||||||
@@ -395,6 +396,7 @@ pub(super) fn binding_slot<'a>(
|
|||||||
("agents", "new_task") => Some(&mut keymap.agents.new_task),
|
("agents", "new_task") => Some(&mut keymap.agents.new_task),
|
||||||
("agents", "rename") => Some(&mut keymap.agents.rename),
|
("agents", "rename") => Some(&mut keymap.agents.rename),
|
||||||
("agents", "stop") => Some(&mut keymap.agents.stop),
|
("agents", "stop") => Some(&mut keymap.agents.stop),
|
||||||
|
("agents", "hide") => Some(&mut keymap.agents.hide),
|
||||||
("agents", "toggle_grouping") => Some(&mut keymap.agents.toggle_grouping),
|
("agents", "toggle_grouping") => Some(&mut keymap.agents.toggle_grouping),
|
||||||
("approval", "open_fullscreen") => Some(&mut keymap.approval.open_fullscreen),
|
("approval", "open_fullscreen") => Some(&mut keymap.approval.open_fullscreen),
|
||||||
("approval", "open_thread") => Some(&mut keymap.approval.open_thread),
|
("approval", "open_thread") => Some(&mut keymap.approval.open_thread),
|
||||||
|
|||||||
+4
-4
@@ -18,11 +18,11 @@ expression: "render_bottom_popup(&app.chat_widget, 48)"
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
New task
|
New task
|
||||||
|
|
||||||
› Describe a new task
|
› Describe a new task
|
||||||
|
|
||||||
↑↓ navigate ctrl+o resume → open ctrl+n new
|
↑↓ navigate ctrl+o resume → open
|
||||||
task ctrl+f search ctrl+s group ctrl+r
|
ctrl+n new task ctrl+f search ctrl+s group
|
||||||
rename ctrl+x stop esc back
|
ctrl+r rename ctrl+x stop ctrl+w hide
|
||||||
|
esc back
|
||||||
+1
-1
@@ -5,7 +5,7 @@ expression: "render_picker(params, 120)"
|
|||||||
|
|
||||||
Keymap
|
Keymap
|
||||||
All configurable shortcuts.
|
All configurable shortcuts.
|
||||||
142 actions, 1 customized, 5 unbound.
|
143 actions, 1 customized, 5 unbound.
|
||||||
|
|
||||||
[All] Common Customized (1) Unbound (5) App Composer Editor Vim Navigation Agents Approval Debug
|
[All] Common Customized (1) Unbound (5) App Composer Editor Vim Navigation Agents Approval Debug
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ expression: "render_picker(params, 120)"
|
|||||||
|
|
||||||
Keymap
|
Keymap
|
||||||
All configurable shortcuts.
|
All configurable shortcuts.
|
||||||
143 actions, 0 customized, 6 unbound.
|
144 actions, 0 customized, 6 unbound.
|
||||||
|
|
||||||
[All] Common Customized (0) Unbound (6) App Composer Editor Vim Navigation Agents Approval Debug
|
[All] Common Customized (0) Unbound (6) App Composer Editor Vim Navigation Agents Approval Debug
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,7 @@
|
|||||||
source: tui/src/keymap_setup.rs
|
source: tui/src/keymap_setup.rs
|
||||||
expression: snapshot
|
expression: snapshot
|
||||||
---
|
---
|
||||||
tab: All (142 selectable)
|
tab: All (143 selectable)
|
||||||
tab: Common (20 selectable)
|
tab: Common (20 selectable)
|
||||||
tab: Customized (0) (0 selectable)
|
tab: Customized (0) (0 selectable)
|
||||||
tab: Unbound (5) (5 selectable)
|
tab: Unbound (5) (5 selectable)
|
||||||
@@ -11,7 +11,7 @@ tab: Composer (5 selectable)
|
|||||||
tab: Editor (17 selectable)
|
tab: Editor (17 selectable)
|
||||||
tab: Vim (69 selectable)
|
tab: Vim (69 selectable)
|
||||||
tab: Navigation (20 selectable)
|
tab: Navigation (20 selectable)
|
||||||
tab: Agents (6 selectable)
|
tab: Agents (7 selectable)
|
||||||
tab: Approval (8 selectable)
|
tab: Approval (8 selectable)
|
||||||
tab: Debug (1 selectable)
|
tab: Debug (1 selectable)
|
||||||
Open Agents | unbound | Global open_agents Open Agents Open the shared agent-session overview. unbound Default
|
Open Agents | unbound | Global open_agents Open Agents Open the shared agent-session overview. unbound Default
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ expression: "render_picker(params, 120)"
|
|||||||
|
|
||||||
Keymap
|
Keymap
|
||||||
All configurable shortcuts.
|
All configurable shortcuts.
|
||||||
142 actions, 1 customized, 6 unbound.
|
143 actions, 1 customized, 6 unbound.
|
||||||
|
|
||||||
[All] Common Customized (1) Unbound (6) App Composer Editor Vim Navigation Agents Approval Debug
|
[All] Common Customized (1) Unbound (6) App Composer Editor Vim Navigation Agents Approval Debug
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ expression: "render_picker(params, 78)"
|
|||||||
|
|
||||||
Keymap
|
Keymap
|
||||||
All configurable shortcuts.
|
All configurable shortcuts.
|
||||||
142 actions, 0 customized, 5 unbound.
|
143 actions, 0 customized, 5 unbound.
|
||||||
|
|
||||||
[All] Common Customized (0) Unbound (5) App Composer Editor Vim
|
[All] Common Customized (0) Unbound (5) App Composer Editor Vim
|
||||||
Navigation Agents Approval Debug
|
Navigation Agents Approval Debug
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ expression: "render_picker(params, 120)"
|
|||||||
|
|
||||||
Keymap
|
Keymap
|
||||||
All configurable shortcuts.
|
All configurable shortcuts.
|
||||||
142 actions, 0 customized, 5 unbound.
|
143 actions, 0 customized, 5 unbound.
|
||||||
|
|
||||||
[All] Common Customized (0) Unbound (5) App Composer Editor Vim Navigation Agents Approval Debug
|
[All] Common Customized (0) Unbound (5) App Composer Editor Vim Navigation Agents Approval Debug
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ expression: "render_picker(params, 120)"
|
|||||||
|
|
||||||
Keymap
|
Keymap
|
||||||
All configurable shortcuts.
|
All configurable shortcuts.
|
||||||
142 actions, 0 customized, 5 unbound.
|
143 actions, 0 customized, 5 unbound.
|
||||||
|
|
||||||
[All] Common Customized (0) Unbound (5) App Composer Editor Vim Navigation Agents Approval Debug
|
[All] Common Customized (0) Unbound (5) App Composer Editor Vim Navigation Agents Approval Debug
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user