Compare commits

...

1 Commits

Author SHA1 Message Date
FrozenPandaz 1a1d467126 fix(core): fix selection of main task 2025-04-29 23:31:42 -04:00
7 changed files with 99 additions and 66 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ export declare class ExternalObject<T> {
}
}
export declare class AppLifeCycle {
constructor(tasks: Array<Task>, pinnedTasks: Array<string>, tuiCliArgs: TuiCliArgs, tuiConfig: TuiConfig, titleText: string)
constructor(tasks: Array<Task>, initiatingTasks: Array<string>, pinnedTasks: Array<string>, tuiCliArgs: TuiCliArgs, tuiConfig: TuiConfig, titleText: string)
startCommand(threadCount?: number | undefined | null): void
scheduleTask(task: Task): void
startTasks(tasks: Array<Task>, metadata: object): void
+2 -2
View File
@@ -13,8 +13,8 @@ pub struct Task {
pub target: TaskTarget,
pub outputs: Vec<String>,
pub project_root: Option<String>,
pub start_time: Option<f64>,
pub end_time: Option<f64>,
pub start_time: Option<i64>,
pub end_time: Option<i64>,
pub continuous: Option<bool>,
}
+3 -1
View File
@@ -1,5 +1,6 @@
use color_eyre::eyre::Result;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEventKind};
use hashbrown::HashSet;
use napi::bindgen_prelude::External;
use napi::threadsafe_function::{ErrorStrategy, ThreadsafeFunction};
use ratatui::layout::{Alignment, Rect};
@@ -52,11 +53,12 @@ pub enum Focus {
impl App {
pub fn new(
tasks: Vec<Task>,
initiating_tasks: HashSet<String>,
pinned_tasks: Vec<String>,
tui_config: TuiConfig,
title_text: String,
) -> Result<Self> {
let tasks_list = TasksList::new(tasks, pinned_tasks, title_text);
let tasks_list = TasksList::new(tasks, initiating_tasks, pinned_tasks, title_text);
let help_popup = HelpPopup::new();
let countdown_popup = CountdownPopup::new();
let focus = tasks_list.get_focus();
@@ -1,5 +1,6 @@
use color_eyre::eyre::Result;
use crossterm::event::KeyEvent;
use hashbrown::HashSet;
use napi::bindgen_prelude::External;
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
@@ -48,9 +49,8 @@ pub struct TaskItem {
pub status: TaskStatus,
terminal_output: String,
pub continuous: bool,
start_time: Option<u128>,
// Public to aid with sorting utility and testing
pub end_time: Option<u128>,
pub start_time: Option<i64>,
pub end_time: Option<i64>,
}
impl Clone for TaskItem {
@@ -154,7 +154,10 @@ impl std::str::FromStr for TaskStatus {
#[napi]
pub fn parse_task_status(string_status: String) -> napi::Result<TaskStatus> {
string_status.as_str().parse().map_err(napi::Error::from_reason)
string_status
.as_str()
.parse()
.map_err(napi::Error::from_reason)
}
/// A list component that displays and manages tasks in a terminal UI.
@@ -163,8 +166,9 @@ pub struct TasksList {
// task id -> pty instance
pub pty_instances: HashMap<String, Arc<PtyInstance>>,
selection_manager: TaskSelectionManager,
pub tasks: Vec<TaskItem>, // Source of truth - all tasks
filtered_names: Vec<String>, // Names of tasks that match the filter
pub tasks: Vec<TaskItem>, // Source of truth - all tasks
initiating_tasks: HashSet<String>, // IDs of tasks which initiated the command
filtered_names: Vec<String>, // Names of tasks that match the filter
throbber_counter: usize,
pub filter_mode: bool,
filter_text: String,
@@ -186,7 +190,12 @@ pub struct TasksList {
impl TasksList {
/// Creates a new TasksList with the given tasks.
/// Converts the input tasks into TaskItems and initializes the UI state.
pub fn new(tasks: Vec<Task>, pinned_tasks: Vec<String>, title_text: String) -> Self {
pub fn new(
tasks: Vec<Task>,
initiating_tasks: HashSet<String>,
pinned_tasks: Vec<String>,
title_text: String,
) -> Self {
let mut task_items = Vec::new();
for task in tasks {
@@ -210,11 +219,12 @@ impl TasksList {
let mut iter = pinned_tasks.into_iter().take(2);
let pane_tasks = [iter.next(), iter.next()];
Self {
let mut s = Self {
pty_instances: HashMap::new(),
selection_manager,
filtered_names,
tasks: task_items,
initiating_tasks,
throbber_counter: 0,
filter_mode: false,
filter_text: String::new(),
@@ -231,7 +241,12 @@ impl TasksList {
title_text,
resize_debounce_timer: None,
pending_resize: None,
}
};
// Sort tasks to populate task selection list
s.sort_tasks();
s
}
pub fn set_max_parallel(&mut self, max_parallel: Option<u32>) {
@@ -842,7 +857,7 @@ impl TasksList {
self.selection_manager.set_selection_mode(mode);
// Sort the tasks
sort_task_items(&mut self.tasks);
sort_task_items(&mut self.tasks, &self.initiating_tasks);
// Update filtered indices to match new order
self.filtered_names = self.tasks.iter().map(|t| t.name.clone()).collect();
@@ -954,6 +969,7 @@ impl TasksList {
for task in tasks {
if let Some(task_item) = self.tasks.iter_mut().find(|t| t.name == task.id) {
task_item.update_status(TaskStatus::InProgress);
task_item.start_time = task.start_time;
}
}
self.sort_tasks();
@@ -966,7 +982,10 @@ impl TasksList {
self.sort_tasks();
}
for (i, data) in self.terminal_pane_data.iter_mut().enumerate() {
if self.pane_tasks.as_ref()[i].clone().is_some_and(|id| id == task_id) {
if self.pane_tasks.as_ref()[i]
.clone()
.is_some_and(|id| id == task_id)
{
let in_progress = status == TaskStatus::InProgress;
data.can_be_interactive = in_progress;
if !in_progress {
@@ -974,7 +993,6 @@ impl TasksList {
}
}
}
}
pub fn end_tasks(&mut self, task_results: Vec<TaskResult>) {
@@ -985,11 +1003,11 @@ impl TasksList {
.find(|t| t.name == task_result.task.id)
{
if task_result.task.start_time.is_some() && task_result.task.end_time.is_some() {
task.start_time = Some(task_result.task.start_time.unwrap() as u128);
task.end_time = Some(task_result.task.end_time.unwrap() as u128);
task.start_time = Some(task_result.task.start_time.unwrap());
task.end_time = Some(task_result.task.end_time.unwrap());
task.duration = utils::format_duration_since(
task_result.task.start_time.unwrap() as u128,
task_result.task.end_time.unwrap() as u128,
task_result.task.start_time.unwrap(),
task_result.task.end_time.unwrap(),
);
}
}
@@ -2274,30 +2292,3 @@ impl Component for TasksList {
self
}
}
impl Default for TasksList {
fn default() -> Self {
Self {
pty_instances: HashMap::new(),
selection_manager: TaskSelectionManager::default(),
tasks: Vec::new(),
filtered_names: Vec::new(),
throbber_counter: 0,
filter_mode: false,
filter_text: String::new(),
filter_persisted: false,
focus: Focus::TaskList,
pane_tasks: [None, None],
focused_pane: None,
is_dimmed: false,
spacebar_mode: false,
terminal_pane_data: [TerminalPaneData::default(), TerminalPaneData::default()],
task_list_hidden: false,
cloud_message: None,
max_parallel: DEFAULT_MAX_PARALLEL,
title_text: String::new(),
resize_debounce_timer: None,
pending_resize: None,
}
}
}
+4
View File
@@ -64,6 +64,7 @@ impl AppLifeCycle {
#[napi(constructor)]
pub fn new(
tasks: Vec<Task>,
initiating_tasks: Vec<String>,
pinned_tasks: Vec<String>,
tui_cli_args: TuiCliArgs,
tui_config: TuiConfig,
@@ -75,10 +76,13 @@ impl AppLifeCycle {
// Convert JSON TUI configuration to our Rust TuiConfig
let rust_tui_config = RustTuiConfig::from((tui_config, &rust_tui_cli_args));
let initiating_tasks = initiating_tasks.into_iter().collect();
Self {
app: Arc::new(std::sync::Mutex::new(
App::new(
tasks.into_iter().map(|t| t.into()).collect(),
initiating_tasks,
pinned_tasks,
rust_tui_config,
title_text,
+54 -19
View File
@@ -1,6 +1,7 @@
use crate::native::tui::components::tasks_list::{TaskItem, TaskStatus};
use hashbrown::HashSet;
pub fn format_duration(duration_ms: u128) -> String {
pub fn format_duration(duration_ms: i64) -> String {
if duration_ms == 0 {
"<1ms".to_string()
} else if duration_ms < 1000 {
@@ -10,7 +11,7 @@ pub fn format_duration(duration_ms: u128) -> String {
}
}
pub fn format_duration_since(start_ms: u128, end_ms: u128) -> String {
pub fn format_duration_since(start_ms: i64, end_ms: i64) -> String {
format_duration(end_ms.saturating_sub(start_ms))
}
@@ -43,7 +44,7 @@ pub fn normalize_newlines(input: &[u8]) -> Vec<u8> {
/// Within each status category:
/// - For completed tasks: sort by end_time if available, then by name
/// - For other statuses: sort by name
pub fn sort_task_items(tasks: &mut [TaskItem]) {
pub fn sort_task_items(tasks: &mut [TaskItem], initating_tasks: &HashSet<String>) {
tasks.sort_by(|a, b| {
// Map status to a numeric category for sorting
let status_to_category = |status: &TaskStatus| -> u8 {
@@ -68,6 +69,20 @@ pub fn sort_task_items(tasks: &mut [TaskItem]) {
return a_category.cmp(&b_category);
}
if a_category == 0 {
match (a.start_time, b.start_time) {
(Some(time_a), Some(time_b)) => {
let time_cmp = time_a.cmp(&time_b);
if time_cmp != std::cmp::Ordering::Equal {
return time_cmp;
}
}
(Some(_), None) => return std::cmp::Ordering::Less,
(None, Some(_)) => return std::cmp::Ordering::Greater,
(None, None) => {}
}
}
// For completed tasks, sort by end_time if available
if a_category == 1 || a_category == 2 {
// Failure or Success categories
@@ -84,8 +99,15 @@ pub fn sort_task_items(tasks: &mut [TaskItem]) {
}
}
// For all other cases or as a tiebreaker, sort by name
a.name.cmp(&b.name)
match (
initating_tasks.contains(&a.name),
initating_tasks.contains(&b.name),
) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
// For all other cases or as a tiebreaker, sort by name
_ => a.name.cmp(&b.name),
}
});
}
@@ -94,7 +116,7 @@ mod tests {
use super::*;
// Helper function to create a TaskItem for testing
fn create_task(name: &str, status: TaskStatus, end_time: Option<u128>) -> TaskItem {
fn create_task(name: &str, status: TaskStatus, end_time: Option<i64>) -> TaskItem {
let mut task = TaskItem::new(name.to_string(), false);
task.status = status;
task.end_time = end_time;
@@ -110,7 +132,7 @@ mod tests {
create_task("task4", TaskStatus::Failure, Some(200)),
];
sort_task_items(&mut tasks);
sort_task_items(&mut tasks, &HashSet::new());
// Expected order: InProgress, Failure, Success, NotStarted
assert_eq!(tasks[0].status, TaskStatus::InProgress);
@@ -127,7 +149,7 @@ mod tests {
create_task("task3", TaskStatus::Success, Some(200)),
];
sort_task_items(&mut tasks);
sort_task_items(&mut tasks, &HashSet::new());
// Should be sorted by end_time: 100, 200, 300
assert_eq!(tasks[0].name, "task2");
@@ -143,7 +165,7 @@ mod tests {
create_task("task3", TaskStatus::Success, None),
];
sort_task_items(&mut tasks);
sort_task_items(&mut tasks, &HashSet::new());
// Tasks with end_time come before those without
assert_eq!(tasks[0].name, "task2");
@@ -160,7 +182,7 @@ mod tests {
create_task("b", TaskStatus::NotStarted, None),
];
sort_task_items(&mut tasks);
sort_task_items(&mut tasks, &HashSet::new());
// Should be sorted alphabetically: a, b, c
assert_eq!(tasks[0].name, "a");
@@ -181,7 +203,7 @@ mod tests {
create_task("s", TaskStatus::NotStarted, None),
];
sort_task_items(&mut tasks);
sort_task_items(&mut tasks, &HashSet::new());
// Expected groups by status:
// 1. InProgress: "u", "y" (alphabetical)
@@ -226,7 +248,7 @@ mod tests {
create_task("b", TaskStatus::Success, Some(100)),
];
sort_task_items(&mut tasks);
sort_task_items(&mut tasks, &HashSet::new());
// When end_times are the same, should sort by name
assert_eq!(tasks[0].name, "a");
@@ -239,17 +261,30 @@ mod tests {
let mut tasks: Vec<TaskItem> = vec![];
// Should not panic on empty list
sort_task_items(&mut tasks);
sort_task_items(&mut tasks, &HashSet::new());
assert!(tasks.is_empty());
}
#[test]
fn test_sort_initiating_tasks() {
let mut tasks = vec![create_task("a", TaskStatus::NotStarted, None), create_task("b", TaskStatus::NotStarted, None)];
// Should not change a single-element list
let mut initiating_tasks = HashSet::new();
initiating_tasks.insert("b".to_string());
sort_task_items(&mut tasks, &initiating_tasks);
assert_eq!(tasks[0].name, "b");
assert_eq!(tasks[0].name, "a");
}
#[test]
fn test_sort_single_task() {
let mut tasks = vec![create_task("task", TaskStatus::Success, Some(100))];
// Should not change a single-element list
sort_task_items(&mut tasks);
sort_task_items(&mut tasks, &HashSet::new());
assert_eq!(tasks.len(), 1);
assert_eq!(tasks[0].name, "task");
@@ -267,7 +302,7 @@ mod tests {
let original_names = tasks.iter().map(|t| t.name.clone()).collect::<Vec<_>>();
// Sort should maintain original order for equal elements
sort_task_items(&mut tasks);
sort_task_items(&mut tasks, &HashSet::new());
let sorted_names = tasks.iter().map(|t| t.name.clone()).collect::<Vec<_>>();
assert_eq!(sorted_names, original_names);
@@ -304,7 +339,7 @@ mod tests {
.collect();
// Sort should not panic with large random dataset
sort_task_items(&mut tasks);
sort_task_items(&mut tasks, &HashSet::new());
// Verify the sort maintains the expected ordering rules
for i in 1..tasks.len() {
@@ -372,12 +407,12 @@ mod tests {
fn test_sort_edge_cases() {
// Test with extreme end_time values
let mut tasks = vec![
create_task("a", TaskStatus::Success, Some(u128::MAX)),
create_task("a", TaskStatus::Success, Some(i64::MAX)),
create_task("b", TaskStatus::Success, Some(0)),
create_task("c", TaskStatus::Success, Some(u128::MAX / 2)),
create_task("c", TaskStatus::Success, Some(i64::MAX / 2)),
];
sort_task_items(&mut tasks);
sort_task_items(&mut tasks, &HashSet::new());
// Should sort by end_time: 0, MAX/2, MAX
assert_eq!(tasks[0].name, "b");
@@ -188,6 +188,7 @@ async function getTerminalOutputLifeCycle(
if (tasks.length > 0) {
appLifeCycle = new AppLifeCycle(
tasks,
initiatingTasks.map((t) => t.id),
pinnedTasks,
nxArgs ?? {},
nxJson.tui ?? {},