Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a1d467126 |
Vendored
+1
-1
@@ -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
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ?? {},
|
||||
|
||||
Reference in New Issue
Block a user