mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-10 11:23:32 +08:00
feat: make ecc2 pane navigation shortcuts configurable
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -37,11 +38,34 @@ pub struct Config {
|
||||
pub token_budget: u64,
|
||||
pub theme: Theme,
|
||||
pub pane_layout: PaneLayout,
|
||||
pub pane_navigation: PaneNavigationConfig,
|
||||
pub linear_pane_size_percent: u16,
|
||||
pub grid_pane_size_percent: u16,
|
||||
pub risk_thresholds: RiskThresholds,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct PaneNavigationConfig {
|
||||
pub focus_sessions: String,
|
||||
pub focus_output: String,
|
||||
pub focus_metrics: String,
|
||||
pub focus_log: String,
|
||||
pub move_left: String,
|
||||
pub move_down: String,
|
||||
pub move_up: String,
|
||||
pub move_right: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PaneNavigationAction {
|
||||
FocusSlot(usize),
|
||||
MoveLeft,
|
||||
MoveDown,
|
||||
MoveUp,
|
||||
MoveRight,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Theme {
|
||||
Dark,
|
||||
@@ -67,6 +91,7 @@ impl Default for Config {
|
||||
token_budget: 500_000,
|
||||
theme: Theme::Dark,
|
||||
pane_layout: PaneLayout::Horizontal,
|
||||
pane_navigation: PaneNavigationConfig::default(),
|
||||
linear_pane_size_percent: 35,
|
||||
grid_pane_size_percent: 50,
|
||||
risk_thresholds: Self::RISK_THRESHOLDS,
|
||||
@@ -115,6 +140,117 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PaneNavigationConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
focus_sessions: "1".to_string(),
|
||||
focus_output: "2".to_string(),
|
||||
focus_metrics: "3".to_string(),
|
||||
focus_log: "4".to_string(),
|
||||
move_left: "ctrl-h".to_string(),
|
||||
move_down: "ctrl-j".to_string(),
|
||||
move_up: "ctrl-k".to_string(),
|
||||
move_right: "ctrl-l".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PaneNavigationConfig {
|
||||
pub fn action_for_key(&self, key: KeyEvent) -> Option<PaneNavigationAction> {
|
||||
[
|
||||
(&self.focus_sessions, PaneNavigationAction::FocusSlot(1)),
|
||||
(&self.focus_output, PaneNavigationAction::FocusSlot(2)),
|
||||
(&self.focus_metrics, PaneNavigationAction::FocusSlot(3)),
|
||||
(&self.focus_log, PaneNavigationAction::FocusSlot(4)),
|
||||
(&self.move_left, PaneNavigationAction::MoveLeft),
|
||||
(&self.move_down, PaneNavigationAction::MoveDown),
|
||||
(&self.move_up, PaneNavigationAction::MoveUp),
|
||||
(&self.move_right, PaneNavigationAction::MoveRight),
|
||||
]
|
||||
.into_iter()
|
||||
.find_map(|(binding, action)| shortcut_matches(binding, key).then_some(action))
|
||||
}
|
||||
|
||||
pub fn focus_shortcuts_label(&self) -> String {
|
||||
[
|
||||
self.focus_sessions.as_str(),
|
||||
self.focus_output.as_str(),
|
||||
self.focus_metrics.as_str(),
|
||||
self.focus_log.as_str(),
|
||||
]
|
||||
.into_iter()
|
||||
.map(shortcut_label)
|
||||
.collect::<Vec<_>>()
|
||||
.join("/")
|
||||
}
|
||||
|
||||
pub fn movement_shortcuts_label(&self) -> String {
|
||||
[
|
||||
self.move_left.as_str(),
|
||||
self.move_down.as_str(),
|
||||
self.move_up.as_str(),
|
||||
self.move_right.as_str(),
|
||||
]
|
||||
.into_iter()
|
||||
.map(shortcut_label)
|
||||
.collect::<Vec<_>>()
|
||||
.join("/")
|
||||
}
|
||||
}
|
||||
|
||||
fn shortcut_matches(spec: &str, key: KeyEvent) -> bool {
|
||||
parse_shortcut(spec).is_some_and(|(modifiers, code)| key.modifiers == modifiers && key.code == code)
|
||||
}
|
||||
|
||||
fn parse_shortcut(spec: &str) -> Option<(KeyModifiers, KeyCode)> {
|
||||
let normalized = spec.trim().to_ascii_lowercase().replace('+', "-");
|
||||
if normalized.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if normalized == "tab" {
|
||||
return Some((KeyModifiers::NONE, KeyCode::Tab));
|
||||
}
|
||||
|
||||
if normalized == "shift-tab" || normalized == "s-tab" {
|
||||
return Some((KeyModifiers::SHIFT, KeyCode::BackTab));
|
||||
}
|
||||
|
||||
if let Some(rest) = normalized
|
||||
.strip_prefix("ctrl-")
|
||||
.or_else(|| normalized.strip_prefix("c-"))
|
||||
{
|
||||
return parse_single_char(rest).map(|ch| (KeyModifiers::CONTROL, KeyCode::Char(ch)));
|
||||
}
|
||||
|
||||
parse_single_char(&normalized).map(|ch| (KeyModifiers::NONE, KeyCode::Char(ch)))
|
||||
}
|
||||
|
||||
fn parse_single_char(value: &str) -> Option<char> {
|
||||
let mut chars = value.chars();
|
||||
let ch = chars.next()?;
|
||||
(chars.next().is_none()).then_some(ch)
|
||||
}
|
||||
|
||||
fn shortcut_label(spec: &str) -> String {
|
||||
let normalized = spec.trim().to_ascii_lowercase().replace('+', "-");
|
||||
if normalized == "tab" {
|
||||
return "Tab".to_string();
|
||||
}
|
||||
if normalized == "shift-tab" || normalized == "s-tab" {
|
||||
return "S-Tab".to_string();
|
||||
}
|
||||
if let Some(rest) = normalized
|
||||
.strip_prefix("ctrl-")
|
||||
.or_else(|| normalized.strip_prefix("c-"))
|
||||
{
|
||||
if let Some(ch) = parse_single_char(rest) {
|
||||
return format!("Ctrl+{ch}");
|
||||
}
|
||||
}
|
||||
normalized
|
||||
}
|
||||
|
||||
impl Default for RiskThresholds {
|
||||
fn default() -> Self {
|
||||
Config::RISK_THRESHOLDS
|
||||
@@ -124,6 +260,7 @@ impl Default for RiskThresholds {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Config, PaneLayout};
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[test]
|
||||
@@ -153,6 +290,7 @@ theme = "Dark"
|
||||
assert_eq!(config.cost_budget_usd, defaults.cost_budget_usd);
|
||||
assert_eq!(config.token_budget, defaults.token_budget);
|
||||
assert_eq!(config.pane_layout, defaults.pane_layout);
|
||||
assert_eq!(config.pane_navigation, defaults.pane_navigation);
|
||||
assert_eq!(
|
||||
config.linear_pane_size_percent,
|
||||
defaults.linear_pane_size_percent
|
||||
@@ -197,6 +335,70 @@ theme = "Dark"
|
||||
assert_eq!(config.pane_layout, PaneLayout::Grid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pane_navigation_deserializes_from_toml() {
|
||||
let config: Config = toml::from_str(
|
||||
r#"
|
||||
[pane_navigation]
|
||||
focus_sessions = "q"
|
||||
focus_output = "w"
|
||||
focus_metrics = "e"
|
||||
focus_log = "r"
|
||||
move_left = "a"
|
||||
move_down = "s"
|
||||
move_up = "w"
|
||||
move_right = "d"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(config.pane_navigation.focus_sessions, "q");
|
||||
assert_eq!(config.pane_navigation.focus_output, "w");
|
||||
assert_eq!(config.pane_navigation.focus_metrics, "e");
|
||||
assert_eq!(config.pane_navigation.focus_log, "r");
|
||||
assert_eq!(config.pane_navigation.move_left, "a");
|
||||
assert_eq!(config.pane_navigation.move_down, "s");
|
||||
assert_eq!(config.pane_navigation.move_up, "w");
|
||||
assert_eq!(config.pane_navigation.move_right, "d");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pane_navigation_matches_default_shortcuts() {
|
||||
let navigation = Config::default().pane_navigation;
|
||||
|
||||
assert_eq!(
|
||||
navigation.action_for_key(KeyEvent::new(KeyCode::Char('1'), KeyModifiers::NONE)),
|
||||
Some(super::PaneNavigationAction::FocusSlot(1))
|
||||
);
|
||||
assert_eq!(
|
||||
navigation.action_for_key(KeyEvent::new(KeyCode::Char('l'), KeyModifiers::CONTROL)),
|
||||
Some(super::PaneNavigationAction::MoveRight)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pane_navigation_matches_custom_shortcuts() {
|
||||
let navigation = super::PaneNavigationConfig {
|
||||
focus_sessions: "q".to_string(),
|
||||
focus_output: "w".to_string(),
|
||||
focus_metrics: "e".to_string(),
|
||||
focus_log: "r".to_string(),
|
||||
move_left: "a".to_string(),
|
||||
move_down: "s".to_string(),
|
||||
move_up: "w".to_string(),
|
||||
move_right: "d".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
navigation.action_for_key(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::NONE)),
|
||||
Some(super::PaneNavigationAction::FocusSlot(3))
|
||||
);
|
||||
assert_eq!(
|
||||
navigation.action_for_key(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE)),
|
||||
Some(super::PaneNavigationAction::MoveRight)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_risk_thresholds_are_applied() {
|
||||
assert_eq!(Config::default().risk_thresholds, Config::RISK_THRESHOLDS);
|
||||
@@ -210,6 +412,8 @@ theme = "Dark"
|
||||
config.auto_dispatch_limit_per_session = 9;
|
||||
config.auto_create_worktrees = false;
|
||||
config.auto_merge_ready_worktrees = true;
|
||||
config.pane_navigation.focus_metrics = "e".to_string();
|
||||
config.pane_navigation.move_right = "d".to_string();
|
||||
config.linear_pane_size_percent = 42;
|
||||
config.grid_pane_size_percent = 55;
|
||||
|
||||
@@ -221,6 +425,8 @@ theme = "Dark"
|
||||
assert_eq!(loaded.auto_dispatch_limit_per_session, 9);
|
||||
assert!(!loaded.auto_create_worktrees);
|
||||
assert!(loaded.auto_merge_ready_worktrees);
|
||||
assert_eq!(loaded.pane_navigation.focus_metrics, "e");
|
||||
assert_eq!(loaded.pane_navigation.move_right, "d");
|
||||
assert_eq!(loaded.linear_pane_size_percent, 42);
|
||||
assert_eq!(loaded.grid_pane_size_percent, 55);
|
||||
|
||||
|
||||
@@ -1664,6 +1664,7 @@ mod tests {
|
||||
token_budget: 500_000,
|
||||
theme: Theme::Dark,
|
||||
pane_layout: PaneLayout::Horizontal,
|
||||
pane_navigation: Default::default(),
|
||||
linear_pane_size_percent: 35,
|
||||
grid_pane_size_percent: 50,
|
||||
risk_thresholds: Config::RISK_THRESHOLDS,
|
||||
|
||||
@@ -47,15 +47,8 @@ pub async fn run(db: StateStore, cfg: Config) -> Result<()> {
|
||||
|
||||
match (key.modifiers, key.code) {
|
||||
(KeyModifiers::CONTROL, KeyCode::Char('c')) => break,
|
||||
(KeyModifiers::CONTROL, KeyCode::Char('h')) => dashboard.focus_pane_left(),
|
||||
(KeyModifiers::CONTROL, KeyCode::Char('j')) => dashboard.focus_pane_down(),
|
||||
(KeyModifiers::CONTROL, KeyCode::Char('k')) => dashboard.focus_pane_up(),
|
||||
(KeyModifiers::CONTROL, KeyCode::Char('l')) => dashboard.focus_pane_right(),
|
||||
(_, KeyCode::Char('q')) => break,
|
||||
(_, KeyCode::Char('1')) => dashboard.focus_pane_number(1),
|
||||
(_, KeyCode::Char('2')) => dashboard.focus_pane_number(2),
|
||||
(_, KeyCode::Char('3')) => dashboard.focus_pane_number(3),
|
||||
(_, KeyCode::Char('4')) => dashboard.focus_pane_number(4),
|
||||
_ if dashboard.handle_pane_navigation_key(key) => {}
|
||||
(_, KeyCode::Tab) => dashboard.next_pane(),
|
||||
(KeyModifiers::SHIFT, KeyCode::BackTab) => dashboard.prev_pane(),
|
||||
(_, KeyCode::Char('+')) | (_, KeyCode::Char('=')) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use chrono::{Duration, Utc};
|
||||
use crossterm::event::KeyEvent;
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{
|
||||
@@ -11,7 +12,7 @@ use tokio::sync::broadcast;
|
||||
|
||||
use super::widgets::{budget_state, format_currency, format_token_count, BudgetState, TokenMeter};
|
||||
use crate::comms;
|
||||
use crate::config::{Config, PaneLayout, Theme};
|
||||
use crate::config::{Config, PaneLayout, PaneNavigationAction, Theme};
|
||||
use crate::observability::ToolLogEntry;
|
||||
use crate::session::manager;
|
||||
use crate::session::output::{
|
||||
@@ -883,7 +884,9 @@ impl Dashboard {
|
||||
|
||||
fn render_status_bar(&self, frame: &mut Frame, area: Rect) {
|
||||
let base_text = format!(
|
||||
" [n]ew session natural spawn [N] [a]ssign re[b]alance global re[B]alance dra[i]n inbox approval jump [I] [g]lobal dispatch coordinate [G]lobal collapse pane [h] restore panes [H] timeline [y] timeline filter [E] [v]iew diff conflict proto[c]ol cont[e]nt filter time [f]ilter scope [A] agent filter [o] [m]erge merge ready [M] auto-worktree [t] auto-merge [w] toggle [p]olicy [,/.] dispatch limit [s]top [u]resume [x]cleanup prune inactive [X] [d]elete [r]efresh [1-4] focus pane [Tab] cycle pane [Ctrl+h/j/k/l] move pane [j/k] scroll delegate [ or ] [Enter] open [+/-] resize [l]ayout {} [T]heme {} [?] help [q]uit ",
|
||||
" [n]ew session natural spawn [N] [a]ssign re[b]alance global re[B]alance dra[i]n inbox approval jump [I] [g]lobal dispatch coordinate [G]lobal collapse pane [h] restore panes [H] timeline [y] timeline filter [E] [v]iew diff conflict proto[c]ol cont[e]nt filter time [f]ilter scope [A] agent filter [o] [m]erge merge ready [M] auto-worktree [t] auto-merge [w] toggle [p]olicy [,/.] dispatch limit [s]top [u]resume [x]cleanup prune inactive [X] [d]elete [r]efresh [{}] focus pane [Tab] cycle pane [{}] move pane [j/k] scroll delegate [ or ] [Enter] open [+/-] resize [l]ayout {} [T]heme {} [?] help [q]uit ",
|
||||
self.pane_focus_shortcuts_label(),
|
||||
self.pane_move_shortcuts_label(),
|
||||
self.layout_label(),
|
||||
self.theme_label()
|
||||
);
|
||||
@@ -956,56 +959,62 @@ impl Dashboard {
|
||||
|
||||
fn render_help(&self, frame: &mut Frame, area: Rect) {
|
||||
let help = vec![
|
||||
"Keyboard Shortcuts:",
|
||||
"",
|
||||
" n New session",
|
||||
" N Natural-language multi-agent spawn prompt",
|
||||
" a Assign follow-up work from selected session",
|
||||
" b Rebalance backed-up delegate handoff backlog for selected lead",
|
||||
" B Rebalance backed-up delegate handoff backlog across lead teams",
|
||||
" i Drain unread task handoffs from selected lead",
|
||||
" I Jump to the next unread approval/conflict target session",
|
||||
" g Auto-dispatch unread handoffs across lead sessions",
|
||||
" G Dispatch then rebalance backlog across lead teams",
|
||||
" h Collapse the focused non-session pane",
|
||||
" H Restore all collapsed panes",
|
||||
" y Toggle selected-session timeline view",
|
||||
" E Cycle timeline event filter",
|
||||
" v Toggle selected worktree diff in output pane",
|
||||
" c Show conflict-resolution protocol for selected conflicted worktree",
|
||||
" e Cycle output content filter: all/errors/tool calls/file changes",
|
||||
" f Cycle output or timeline time range between all/15m/1h/24h",
|
||||
" A Toggle search or timeline scope between selected session and all sessions",
|
||||
" o Toggle search agent filter between all agents and selected agent type",
|
||||
" m Merge selected ready worktree into base and clean it up",
|
||||
" M Merge all ready inactive worktrees and clean them up",
|
||||
" l Cycle pane layout and persist it",
|
||||
" T Toggle theme and persist it",
|
||||
" t Toggle default worktree creation for new sessions and delegated work",
|
||||
" p Toggle daemon auto-dispatch policy and persist config",
|
||||
" w Toggle daemon auto-merge for ready inactive worktrees",
|
||||
" ,/. Decrease/increase auto-dispatch limit per lead",
|
||||
" s Stop selected session",
|
||||
" u Resume selected session",
|
||||
" x Cleanup selected worktree",
|
||||
" X Prune inactive worktrees globally",
|
||||
" d Delete selected inactive session",
|
||||
" 1-4 Focus Sessions/Output/Metrics/Log directly",
|
||||
" Tab Next pane",
|
||||
" S-Tab Previous pane",
|
||||
" C-hjkl Move pane focus left/down/up/right",
|
||||
" j/↓ Scroll down",
|
||||
" k/↑ Scroll up",
|
||||
" [ or ] Focus previous/next delegate in lead Metrics board",
|
||||
" Enter Open focused delegate from lead Metrics board",
|
||||
" / Search current session output",
|
||||
" n/N Next/previous search match when search is active",
|
||||
" Esc Clear active search or cancel search input",
|
||||
" +/= Increase pane size and persist it",
|
||||
" - Decrease pane size and persist it",
|
||||
" r Refresh",
|
||||
" ? Toggle help",
|
||||
" q/C-c Quit",
|
||||
"Keyboard Shortcuts:".to_string(),
|
||||
"".to_string(),
|
||||
" n New session".to_string(),
|
||||
" N Natural-language multi-agent spawn prompt".to_string(),
|
||||
" a Assign follow-up work from selected session".to_string(),
|
||||
" b Rebalance backed-up delegate handoff backlog for selected lead".to_string(),
|
||||
" B Rebalance backed-up delegate handoff backlog across lead teams".to_string(),
|
||||
" i Drain unread task handoffs from selected lead".to_string(),
|
||||
" I Jump to the next unread approval/conflict target session".to_string(),
|
||||
" g Auto-dispatch unread handoffs across lead sessions".to_string(),
|
||||
" G Dispatch then rebalance backlog across lead teams".to_string(),
|
||||
" h Collapse the focused non-session pane".to_string(),
|
||||
" H Restore all collapsed panes".to_string(),
|
||||
" y Toggle selected-session timeline view".to_string(),
|
||||
" E Cycle timeline event filter".to_string(),
|
||||
" v Toggle selected worktree diff in output pane".to_string(),
|
||||
" c Show conflict-resolution protocol for selected conflicted worktree".to_string(),
|
||||
" e Cycle output content filter: all/errors/tool calls/file changes".to_string(),
|
||||
" f Cycle output or timeline time range between all/15m/1h/24h".to_string(),
|
||||
" A Toggle search or timeline scope between selected session and all sessions".to_string(),
|
||||
" o Toggle search agent filter between all agents and selected agent type".to_string(),
|
||||
" m Merge selected ready worktree into base and clean it up".to_string(),
|
||||
" M Merge all ready inactive worktrees and clean them up".to_string(),
|
||||
" l Cycle pane layout and persist it".to_string(),
|
||||
" T Toggle theme and persist it".to_string(),
|
||||
" t Toggle default worktree creation for new sessions and delegated work".to_string(),
|
||||
" p Toggle daemon auto-dispatch policy and persist config".to_string(),
|
||||
" w Toggle daemon auto-merge for ready inactive worktrees".to_string(),
|
||||
" ,/. Decrease/increase auto-dispatch limit per lead".to_string(),
|
||||
" s Stop selected session".to_string(),
|
||||
" u Resume selected session".to_string(),
|
||||
" x Cleanup selected worktree".to_string(),
|
||||
" X Prune inactive worktrees globally".to_string(),
|
||||
" d Delete selected inactive session".to_string(),
|
||||
format!(
|
||||
" {:<7} Focus Sessions/Output/Metrics/Log directly",
|
||||
self.pane_focus_shortcuts_label()
|
||||
),
|
||||
" Tab Next pane".to_string(),
|
||||
" S-Tab Previous pane".to_string(),
|
||||
format!(
|
||||
" {:<7} Move pane focus left/down/up/right",
|
||||
self.pane_move_shortcuts_label()
|
||||
),
|
||||
" j/↓ Scroll down".to_string(),
|
||||
" k/↑ Scroll up".to_string(),
|
||||
" [ or ] Focus previous/next delegate in lead Metrics board".to_string(),
|
||||
" Enter Open focused delegate from lead Metrics board".to_string(),
|
||||
" / Search current session output".to_string(),
|
||||
" n/N Next/previous search match when search is active".to_string(),
|
||||
" Esc Clear active search or cancel search input".to_string(),
|
||||
" +/= Increase pane size and persist it".to_string(),
|
||||
" - Decrease pane size and persist it".to_string(),
|
||||
" r Refresh".to_string(),
|
||||
" ? Toggle help".to_string(),
|
||||
" q/C-c Quit".to_string(),
|
||||
];
|
||||
|
||||
let paragraph = Paragraph::new(help.join("\n")).block(
|
||||
@@ -1072,6 +1081,32 @@ impl Dashboard {
|
||||
self.move_pane_focus(PaneDirection::Down);
|
||||
}
|
||||
|
||||
pub fn handle_pane_navigation_key(&mut self, key: KeyEvent) -> bool {
|
||||
match self.cfg.pane_navigation.action_for_key(key) {
|
||||
Some(PaneNavigationAction::FocusSlot(slot)) => {
|
||||
self.focus_pane_number(slot);
|
||||
true
|
||||
}
|
||||
Some(PaneNavigationAction::MoveLeft) => {
|
||||
self.focus_pane_left();
|
||||
true
|
||||
}
|
||||
Some(PaneNavigationAction::MoveDown) => {
|
||||
self.focus_pane_down();
|
||||
true
|
||||
}
|
||||
Some(PaneNavigationAction::MoveUp) => {
|
||||
self.focus_pane_up();
|
||||
true
|
||||
}
|
||||
Some(PaneNavigationAction::MoveRight) => {
|
||||
self.focus_pane_right();
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn collapse_selected_pane(&mut self) {
|
||||
if self.selected_pane == Pane::Sessions {
|
||||
self.set_operator_note("cannot collapse sessions pane".to_string());
|
||||
@@ -2726,6 +2761,14 @@ impl Dashboard {
|
||||
}
|
||||
}
|
||||
|
||||
fn pane_focus_shortcuts_label(&self) -> String {
|
||||
self.cfg.pane_navigation.focus_shortcuts_label()
|
||||
}
|
||||
|
||||
fn pane_move_shortcuts_label(&self) -> String {
|
||||
self.cfg.pane_navigation.movement_shortcuts_label()
|
||||
}
|
||||
|
||||
fn sync_global_handoff_backlog(&mut self) {
|
||||
let limit = self.sessions.len().max(1);
|
||||
match self.db.unread_task_handoff_targets(limit) {
|
||||
@@ -8393,6 +8436,41 @@ diff --git a/src/next.rs b/src/next.rs
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_pane_navigation_keys_override_defaults() {
|
||||
let mut dashboard = test_dashboard(Vec::new(), 0);
|
||||
dashboard.cfg.pane_navigation.focus_metrics = "e".to_string();
|
||||
dashboard.cfg.pane_navigation.move_left = "a".to_string();
|
||||
|
||||
assert!(dashboard.handle_pane_navigation_key(KeyEvent::new(
|
||||
crossterm::event::KeyCode::Char('e'),
|
||||
crossterm::event::KeyModifiers::NONE,
|
||||
)));
|
||||
assert_eq!(dashboard.selected_pane, Pane::Metrics);
|
||||
|
||||
assert!(dashboard.handle_pane_navigation_key(KeyEvent::new(
|
||||
crossterm::event::KeyCode::Char('a'),
|
||||
crossterm::event::KeyModifiers::NONE,
|
||||
)));
|
||||
assert_eq!(dashboard.selected_pane, Pane::Sessions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pane_navigation_labels_use_configured_bindings() {
|
||||
let mut dashboard = test_dashboard(Vec::new(), 0);
|
||||
dashboard.cfg.pane_navigation.focus_sessions = "q".to_string();
|
||||
dashboard.cfg.pane_navigation.focus_output = "w".to_string();
|
||||
dashboard.cfg.pane_navigation.focus_metrics = "e".to_string();
|
||||
dashboard.cfg.pane_navigation.focus_log = "r".to_string();
|
||||
dashboard.cfg.pane_navigation.move_left = "a".to_string();
|
||||
dashboard.cfg.pane_navigation.move_down = "s".to_string();
|
||||
dashboard.cfg.pane_navigation.move_up = "w".to_string();
|
||||
dashboard.cfg.pane_navigation.move_right = "d".to_string();
|
||||
|
||||
assert_eq!(dashboard.pane_focus_shortcuts_label(), "q/w/e/r");
|
||||
assert_eq!(dashboard.pane_move_shortcuts_label(), "a/s/w/d");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cycle_pane_layout_rotates_and_hides_log_when_leaving_grid() {
|
||||
let mut dashboard = test_dashboard(Vec::new(), 0);
|
||||
@@ -8717,6 +8795,7 @@ diff --git a/src/next.rs b/src/next.rs
|
||||
token_budget: 500_000,
|
||||
theme: Theme::Dark,
|
||||
pane_layout: PaneLayout::Horizontal,
|
||||
pane_navigation: Default::default(),
|
||||
linear_pane_size_percent: 35,
|
||||
grid_pane_size_percent: 50,
|
||||
risk_thresholds: Config::RISK_THRESHOLDS,
|
||||
|
||||
Reference in New Issue
Block a user