From 4222dfd8afc6eb275fe8bf35051d1012ead2985b Mon Sep 17 00:00:00 2001 From: branchseer Date: Wed, 11 Mar 2026 09:56:52 +0800 Subject: [PATCH 1/9] feat: Improve task selector styling with colored labels and aligned commands - Add left-padding to align all commands at the same column - Color task names with cyan, package prefixes with light cyan - Selected line uses uniform bold with no color changes - Split labels into package# prefix and task name for independent styling Co-Authored-By: Claude Opus 4.6 --- crates/vite_select/src/interactive.rs | 168 ++++++++++++++++++++++---- 1 file changed, 146 insertions(+), 22 deletions(-) diff --git a/crates/vite_select/src/interactive.rs b/crates/vite_select/src/interactive.rs index c5022d30..b5ec22c2 100644 --- a/crates/vite_select/src/interactive.rs +++ b/crates/vite_select/src/interactive.rs @@ -97,6 +97,12 @@ impl<'a> State<'a> { } } +/// Split a label like `"package#task"` into `("package#", "task")`. +/// Labels without `#` return `("", label)`. +fn split_label(label: &str) -> (&str, &str) { + label.find('#').map_or(("", label), |pos| (&label[..=pos], &label[pos + 1..])) +} + /// Parameters for rendering a task list. pub struct RenderParams<'a> { pub items: &'a [SelectItem], @@ -122,6 +128,7 @@ pub struct RenderParams<'a> { /// and non-interactive modes (via [`crate::non_interactive`]). /// /// Returns the number of lines written. +#[expect(clippy::too_many_lines, reason = "single rendering function with sequential layout logic")] pub fn render_items(writer: &mut impl Write, params: &RenderParams<'_>) -> anyhow::Result { let RenderParams { items, @@ -164,6 +171,13 @@ pub fn render_items(writer: &mut impl Write, params: &RenderParams<'_>) -> anyho lines += 2; } + // Compute max label width for interactive column alignment + let max_label_width = if is_interactive { + visible_range.clone().map(|vi| items[filtered[vi]].label.chars().count()).max().unwrap_or(0) + } else { + 0 + }; + // Items for vi in visible_range.clone() { let item_idx = filtered[vi]; @@ -174,9 +188,12 @@ pub fn render_items(writer: &mut impl Write, params: &RenderParams<'_>) -> anyho // Line layout: // - interactive prefix is " › " or " " (4 chars) // - non-interactive prefix is " " (2 chars) - // then label + ": " + description + // then label (padded to max_label_width in interactive) + ": " + description + let label_width = item.label.chars().count(); + let padded_label_width = if is_interactive { max_label_width } else { label_width }; + let label_padding = padded_label_width - label_width; let prefix_width = if is_interactive { 4 } else { 2 }; - let prefix_and_label_width = prefix_width + item.label.chars().count() + 2; + let prefix_and_label_width = prefix_width + padded_label_width + 2; let max_desc_chars = params.max_line_width.saturating_sub(prefix_and_label_width); let desc_str = item.description.as_str(); let desc_char_count = desc_str.chars().count(); @@ -191,25 +208,31 @@ pub fn render_items(writer: &mut impl Write, params: &RenderParams<'_>) -> anyho desc_str }; - if is_selected && is_interactive { - write!( - writer, - "{blue}{bold} \u{203a} {label}: {desc}{reset}{line_ending}", - blue = SetForegroundColor(Color::Blue), - bold = SetAttribute(Attribute::Bold), - label = item.label, - desc = display_desc, - reset = SetAttribute(Attribute::Reset), - )?; - } else if is_interactive { - write!( - writer, - "{marker_color} {reset_color}{}:{command_color} {display_desc}{reset_color}{line_ending}", - item.label, - marker_color = SetForegroundColor(Color::DarkGrey), - command_color = SetForegroundColor(Color::Grey), - reset_color = ResetColor, - )?; + if is_interactive { + let (pkg, task) = split_label(&item.label); + if is_selected { + write!( + writer, + "{bold} \u{203a} {:>pad$}{pkg}{task}: {desc}{no_attr}{line_ending}", + "", + pad = label_padding, + bold = SetAttribute(Attribute::Bold), + no_attr = SetAttribute(Attribute::Reset), + desc = display_desc, + )?; + } else { + write!( + writer, + " {:>pad$}{light_cyan}{pkg}{no_color}{cyan}{task}{no_attr}: {desc}{line_ending}", + "", + pad = label_padding, + light_cyan = SetForegroundColor(Color::Rgb { r: 130, g: 200, b: 210 }), + cyan = SetForegroundColor(Color::Cyan), + no_color = ResetColor, + no_attr = SetAttribute(Attribute::Reset), + desc = display_desc, + )?; + } } else if is_selected { write!( writer, @@ -526,6 +549,107 @@ mod tests { assert_eq!(prompt, "Select a task (↑/↓, Enter to run, Esc to clear):"); assert!(spacer.is_empty()); assert_eq!(selected, " › build: echo build"); - assert_eq!(unselected, " lint: echo lint"); + assert_eq!(unselected, " lint: echo lint"); + } + + #[test] + fn interactive_commands_are_aligned() { + let items = + make_items(&[("build", "echo build"), ("lint", "echo lint"), ("test", "vitest run")]); + let output = render_interactive_to_string(&items, "", 80); + let item_lines: Vec<&str> = output.lines().skip(2).collect(); + // max_label_width = 5 ("build"), descriptions should all start at column 11 + // prefix(4) + max_label(5) + ": "(2) = 11 + assert_eq!(item_lines[0], " \u{203a} build: echo build"); + assert_eq!(item_lines[1], " lint: echo lint"); + assert_eq!(item_lines[2], " test: vitest run"); + } + + #[test] + fn interactive_alignment_with_package_labels() { + let items = make_items(&[("app#build", "echo build"), ("lint", "echo lint")]); + let output = render_interactive_to_string(&items, "", 80); + let item_lines: Vec<&str> = output.lines().skip(2).collect(); + // max_label_width = 9 ("app#build"), padding for "lint" = 5 + assert_eq!(item_lines[0], " \u{203a} app#build: echo build"); + assert_eq!(item_lines[1], " lint: echo lint"); + } + + #[test] + fn interactive_truncation_accounts_for_padding() { + let items = make_items(&[ + ("build", "a really long command that exceeds the width limit"), + ("lint", "short"), + ]); + // max_label_width = 5, prefix(4) + max_label(5) + sep(2) = 11 + // max_line_width = 30 => max_desc = 30 - 11 = 19 chars + let output = render_interactive_to_string(&items, "", 30); + for line in output.lines().skip(2) { + assert!( + line.chars().count() <= 30, + "line exceeds 30 chars: ({}) {line:?}", + line.chars().count() + ); + } + let build_line = output.lines().nth(2).unwrap(); + assert!( + build_line.contains('\u{2026}'), + "long description should be truncated: {build_line:?}" + ); + } + + #[test] + fn interactive_left_padding_aligns_commands() { + let items = make_items(&[ + ("app#build", "echo build"), + ("app#lint", "echo lint"), + ("lib#typecheck", "echo tc"), + ]); + let output = render_interactive_to_string(&items, "", 80); + let item_lines: Vec<&str> = output.lines().skip(2).collect(); + // max_label_width = 13 ("lib#typecheck") + // "app#build" = 9, padding = 4; "app#lint" = 8, padding = 5 + // Padding goes before the label (left side), colon always followed by single space + // "lib#typecheck" = 13 (max), "app#build" = 9 (pad 4), "app#lint" = 8 (pad 5) + assert_eq!(item_lines[0], " \u{203a} app#build: echo build"); + assert_eq!(item_lines[1], " app#lint: echo lint"); + assert_eq!(item_lines[2], " lib#typecheck: echo tc"); + + // Verify all ": " separators are at the same char column + let colon_columns: Vec = + item_lines.iter().map(|l| l.chars().take_while(|&c| c != ':').count()).collect(); + assert!( + colon_columns.windows(2).all(|w| w[0] == w[1]), + "colon columns should be aligned: {colon_columns:?}" + ); + } + + #[test] + fn interactive_left_padding_with_truncation_preserves_ellipsis() { + let items = make_items(&[ + ("app#build", "a really long command that exceeds the width limit"), + ("lint", "short"), + ]); + // max_label_width = 9 ("app#build"), prefix(4) + 9 + sep(2) = 15 + // max_line_width = 30 => max_desc = 30 - 15 = 15 chars + let output = render_interactive_to_string(&items, "", 30); + for line in output.lines().skip(2) { + assert!( + line.chars().count() <= 30, + "line exceeds 30 chars: ({}) {line:?}", + line.chars().count() + ); + } + let build_line = output.lines().nth(2).unwrap(); + assert!( + build_line.contains('\u{2026}'), + "truncated line should contain ellipsis: {build_line:?}" + ); + // "lint" has 5 chars of left padding (9 - 4) + let lint_line = output.lines().nth(3).unwrap(); + assert!( + lint_line.contains(" lint: short"), + "short label should have left padding: {lint_line:?}" + ); } } From 04fb5d890b46bd7e818f0463ec27c7bbf9b3d6bf Mon Sep 17 00:00:00 2001 From: branchseer Date: Wed, 11 Mar 2026 10:00:32 +0800 Subject: [PATCH 2/9] update color --- package.json | 3 ++- packages/tools/package.json | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 07146397..07c6453c 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ }, "type": "module", "scripts": { - "prepare": "husky" + "prepare": "husky", + "build": "echo build" }, "devDependencies": { "@types/node": "catalog:", diff --git a/packages/tools/package.json b/packages/tools/package.json index 769b10ef..9bf26bff 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -11,6 +11,10 @@ "read-stdin": "./src/read-stdin.js", "replace-file-content": "./src/replace-file-content.ts" }, + "scripts": { + "build": "echo foo", + "lint": "vp lint" + }, "dependencies": { "cross-env": "^10.1.0", "oxfmt": "0.26.0", From 46633b4fc4d0f9cb528da9d6b9fdb2c907bf470a Mon Sep 17 00:00:00 2001 From: branchseer Date: Wed, 11 Mar 2026 10:10:06 +0800 Subject: [PATCH 3/9] feat: Move command alignment padding after colon in task selector Place the alignment padding between the colon and command instead of before the label, so commands are visually aligned at the same column. Co-Authored-By: Claude Opus 4.6 --- crates/vite_select/src/interactive.rs | 53 +++++++++++++++------------ 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/crates/vite_select/src/interactive.rs b/crates/vite_select/src/interactive.rs index b5ec22c2..0eda3bc3 100644 --- a/crates/vite_select/src/interactive.rs +++ b/crates/vite_select/src/interactive.rs @@ -213,7 +213,7 @@ pub fn render_items(writer: &mut impl Write, params: &RenderParams<'_>) -> anyho if is_selected { write!( writer, - "{bold} \u{203a} {:>pad$}{pkg}{task}: {desc}{no_attr}{line_ending}", + "{bold} \u{203a} {pkg}{task}:{:>pad$} {desc}{no_attr}{line_ending}", "", pad = label_padding, bold = SetAttribute(Attribute::Bold), @@ -223,7 +223,7 @@ pub fn render_items(writer: &mut impl Write, params: &RenderParams<'_>) -> anyho } else { write!( writer, - " {:>pad$}{light_cyan}{pkg}{no_color}{cyan}{task}{no_attr}: {desc}{line_ending}", + " {light_cyan}{pkg}{no_color}{cyan}{task}{no_attr}:{:>pad$} {desc}{line_ending}", "", pad = label_padding, light_cyan = SetForegroundColor(Color::Rgb { r: 130, g: 200, b: 210 }), @@ -549,7 +549,7 @@ mod tests { assert_eq!(prompt, "Select a task (↑/↓, Enter to run, Esc to clear):"); assert!(spacer.is_empty()); assert_eq!(selected, " › build: echo build"); - assert_eq!(unselected, " lint: echo lint"); + assert_eq!(unselected, " lint: echo lint"); } #[test] @@ -558,11 +558,11 @@ mod tests { make_items(&[("build", "echo build"), ("lint", "echo lint"), ("test", "vitest run")]); let output = render_interactive_to_string(&items, "", 80); let item_lines: Vec<&str> = output.lines().skip(2).collect(); - // max_label_width = 5 ("build"), descriptions should all start at column 11 - // prefix(4) + max_label(5) + ": "(2) = 11 + // max_label_width = 5 ("build") + // prefix(4) + max_label(5) + ":" + padding + " " + desc assert_eq!(item_lines[0], " \u{203a} build: echo build"); - assert_eq!(item_lines[1], " lint: echo lint"); - assert_eq!(item_lines[2], " test: vitest run"); + assert_eq!(item_lines[1], " lint: echo lint"); + assert_eq!(item_lines[2], " test: vitest run"); } #[test] @@ -572,7 +572,7 @@ mod tests { let item_lines: Vec<&str> = output.lines().skip(2).collect(); // max_label_width = 9 ("app#build"), padding for "lint" = 5 assert_eq!(item_lines[0], " \u{203a} app#build: echo build"); - assert_eq!(item_lines[1], " lint: echo lint"); + assert_eq!(item_lines[1], " lint: echo lint"); } #[test] @@ -599,7 +599,7 @@ mod tests { } #[test] - fn interactive_left_padding_aligns_commands() { + fn interactive_padding_aligns_commands() { let items = make_items(&[ ("app#build", "echo build"), ("app#lint", "echo lint"), @@ -608,24 +608,31 @@ mod tests { let output = render_interactive_to_string(&items, "", 80); let item_lines: Vec<&str> = output.lines().skip(2).collect(); // max_label_width = 13 ("lib#typecheck") - // "app#build" = 9, padding = 4; "app#lint" = 8, padding = 5 - // Padding goes before the label (left side), colon always followed by single space - // "lib#typecheck" = 13 (max), "app#build" = 9 (pad 4), "app#lint" = 8 (pad 5) - assert_eq!(item_lines[0], " \u{203a} app#build: echo build"); - assert_eq!(item_lines[1], " app#lint: echo lint"); + // Padding goes after ":" to align commands + assert_eq!(item_lines[0], " \u{203a} app#build: echo build"); + assert_eq!(item_lines[1], " app#lint: echo lint"); assert_eq!(item_lines[2], " lib#typecheck: echo tc"); - // Verify all ": " separators are at the same char column - let colon_columns: Vec = - item_lines.iter().map(|l| l.chars().take_while(|&c| c != ':').count()).collect(); + // Verify all commands start at the same char column + // prefix(4) + max_label(13) + ":" + padding + " " = commands start at column 19 + let cmd_columns: Vec = item_lines + .iter() + .map(|l| { + let colon_pos = l.chars().take_while(|&c| c != ':').count(); + // skip colon, then count padding spaces + the separator space + colon_pos + + 1 + + l[l.find(':').unwrap() + 1..].chars().take_while(|&c| c == ' ').count() + }) + .collect(); assert!( - colon_columns.windows(2).all(|w| w[0] == w[1]), - "colon columns should be aligned: {colon_columns:?}" + cmd_columns.windows(2).all(|w| w[0] == w[1]), + "command columns should be aligned: {cmd_columns:?}" ); } #[test] - fn interactive_left_padding_with_truncation_preserves_ellipsis() { + fn interactive_padding_with_truncation_preserves_ellipsis() { let items = make_items(&[ ("app#build", "a really long command that exceeds the width limit"), ("lint", "short"), @@ -645,11 +652,11 @@ mod tests { build_line.contains('\u{2026}'), "truncated line should contain ellipsis: {build_line:?}" ); - // "lint" has 5 chars of left padding (9 - 4) + // "lint" (4) has padding of 5 after colon (9 - 4) let lint_line = output.lines().nth(3).unwrap(); assert!( - lint_line.contains(" lint: short"), - "short label should have left padding: {lint_line:?}" + lint_line.contains("lint: short"), + "short label should have padding after colon: {lint_line:?}" ); } } From 27bc8a0d55b0283b82118a1a46b9948bc6651b43 Mon Sep 17 00:00:00 2001 From: branchseer Date: Wed, 11 Mar 2026 13:35:31 +0800 Subject: [PATCH 4/9] feat: Use package path instead of name for interactive task selection When selecting a task from the interactive selector (`vp run`), match the package by filesystem path rather than display name. This bypasses CLI specifier parsing and directly constructs a TaskQuery with PackageQuery::containing_package(path). Co-Authored-By: Claude Opus 4.6 --- crates/vite_task/src/session/mod.rs | 74 ++++++++++++++----- .../task-list/snapshots/vp run in script.snap | 10 +-- .../interactive long command truncated.snap | 30 ++++---- ...ve enter with no results does nothing.snap | 40 +++++----- .../interactive escape clears query.snap | 42 +++++------ .../interactive scroll long list.snap | 60 +++++++-------- ...interactive search other package task.snap | 20 ++--- ...earch preserves rating within package.snap | 42 +++++------ .../interactive search then select.snap | 22 +++--- ...active search with hash skips reorder.snap | 26 +++---- .../interactive select task from lib.snap | 20 ++--- .../snapshots/interactive select task.snap | 40 +++++----- .../interactive select with typo.snap | 2 +- .../verbose with typo enters selector.snap | 2 +- crates/vite_workspace/src/package_graph.rs | 13 ++++ 15 files changed, 247 insertions(+), 196 deletions(-) diff --git a/crates/vite_task/src/session/mod.rs b/crates/vite_task/src/session/mod.rs index dfbd2c83..e5b8d0e7 100644 --- a/crates/vite_task/src/session/mod.rs +++ b/crates/vite_task/src/session/mod.rs @@ -21,14 +21,16 @@ use vite_select::SelectItem; use vite_str::Str; use vite_task_graph::{ IndexedTaskGraph, TaskGraph, TaskGraphLoadError, config::user::UserCacheConfig, - loader::UserConfigLoader, + loader::UserConfigLoader, query::TaskQuery, }; use vite_task_plan::{ ExecutionGraph, TaskGraphLoader, - plan_request::{PlanRequest, ScriptCommand, SyntheticPlanRequest}, + plan_request::{ + PlanOptions, PlanRequest, QueryPlanRequest, ScriptCommand, SyntheticPlanRequest, + }, prepend_path_env, }; -use vite_workspace::{WorkspaceRoot, find_workspace_root}; +use vite_workspace::{WorkspaceRoot, find_workspace_root, package_graph::PackageQuery}; use crate::cli::{CacheSubcommand, Command, ResolvedCommand, ResolvedRunCommand, RunCommand}; @@ -272,7 +274,7 @@ impl<'a> Session<'a> { match command.into_resolved() { ResolvedCommand::Cache { ref subcmd } => self.handle_cache_command(subcmd), ResolvedCommand::RunLastDetails => self.show_last_run_details(), - ResolvedCommand::Run(mut run_command) => { + ResolvedCommand::Run(run_command) => { let is_interactive = std::io::stdin().is_terminal() && std::io::stdout().is_terminal(); @@ -286,9 +288,8 @@ impl<'a> Session<'a> { // No tasks matched. With is_cwd_only (no scope flags) the // task name is a typo — show the selector. Otherwise error. if is_cwd_only { - self.handle_no_task(is_interactive, &mut run_command).await?; - let cwd = Arc::clone(&self.cwd); - self.plan_from_cli_run_resolved(cwd, run_command.clone()).await?.0 + let qpr = self.handle_no_task(is_interactive, &run_command).await?; + self.plan_from_query(qpr).await? } else { return Err(vite_task_plan::Error::NoTasksMatched( task_specifier.clone(), @@ -307,9 +308,8 @@ impl<'a> Session<'a> { if run_command != bare { return Err(vite_task_plan::Error::MissingTaskSpecifier.into()); } - self.handle_no_task(is_interactive, &mut run_command).await?; - let cwd = Arc::clone(&self.cwd); - self.plan_from_cli_run_resolved(cwd, run_command.clone()).await?.0 + let qpr = self.handle_no_task(is_interactive, &run_command).await?; + self.plan_from_query(qpr).await? }; let builder = LabeledReporterBuilder::new( @@ -334,11 +334,11 @@ impl<'a> Session<'a> { Ok(()) } - /// Show the task selector or list, and update the run command with the selected task. + /// Show the task selector or list, and return a plan request for the selected task. /// /// In interactive mode, shows a fuzzy-searchable selection list. On selection, - /// updates `run_command.task_specifier` and returns `Ok(())` so the caller - /// can plan and execute the selected task. + /// returns `Ok(QueryPlanRequest)` using the selected entry's filesystem path + /// (not its display name) for package matching. /// /// In non-interactive mode, prints the task list (or "did you mean" suggestions) /// and returns `Err(SessionError::EarlyExit(_))` — no further execution needed. @@ -349,8 +349,8 @@ impl<'a> Session<'a> { async fn handle_no_task( &mut self, is_interactive: bool, - run_command: &mut ResolvedRunCommand, - ) -> Result<(), SessionError> { + run_command: &ResolvedRunCommand, + ) -> Result { let not_found_name = run_command.task_specifier.as_deref(); let cwd = Arc::clone(&self.cwd); let task_graph = self.ensure_task_graph_loaded().await?; @@ -444,7 +444,9 @@ impl<'a> Session<'a> { })); }; - // Interactive: print selected task and run it + // Interactive: print selected task and build a QueryPlanRequest using the + // entry's filesystem path (not its display name) for package matching. + let entry = &entries[selected_index]; let selected_label = &select_items[selected_index].label; { use std::io::Write as _; @@ -457,8 +459,20 @@ impl<'a> Session<'a> { selected_label, )?; } - run_command.task_specifier = Some(selected_label.clone()); - Ok(()) + + let package_query = + PackageQuery::containing_package(Arc::clone(&entry.task_display.package_path)); + Ok(QueryPlanRequest { + query: TaskQuery { + package_query, + task_name: entry.task_display.task_name.clone(), + include_explicit_deps: !run_command.flags.ignore_depends_on, + }, + plan_options: PlanOptions { + extra_args: run_command.additional_args.clone().into(), + cache_override: run_command.flags.cache_override(), + }, + }) } /// Lazily initializes and returns the execution cache. @@ -670,4 +684,28 @@ impl<'a> Session<'a> { .await?; Ok((graph, is_cwd_only)) } + + /// Plan execution from a pre-built [`QueryPlanRequest`]. + /// + /// Used by the interactive task selector, which constructs the request + /// directly (bypassing CLI specifier parsing). + #[expect( + clippy::future_not_send, + reason = "session is single-threaded, futures do not need to be Send" + )] + async fn plan_from_query( + &mut self, + request: QueryPlanRequest, + ) -> Result { + let cwd = Arc::clone(&self.cwd); + vite_task_plan::plan_query( + request, + &self.workspace_path, + &cwd, + &self.envs, + &mut self.plan_request_parser, + &mut self.lazy_task_graph, + ) + .await + } } diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-list/snapshots/vp run in script.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-list/snapshots/vp run in script.snap index e85df174..9202ca69 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-list/snapshots/vp run in script.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-list/snapshots/vp run in script.snap @@ -7,12 +7,12 @@ expression: e2e_outputs $ vp run ⊘ cache disabled Select a task (↑/↓, Enter to run, Esc to clear): - › hello: echo hello from root + › hello: echo hello from root list-tasks: vp run - app#build: echo build app - app#lint: echo lint app - app#test: echo test app - lib#build: echo build lib + app#build: echo build app + app#lint: echo lint app + app#test: echo test app + lib#build: echo build lib @ write-key: enter $ vp run ⊘ cache disabled Selected task: hello diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select-truncate/snapshots/interactive long command truncated.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select-truncate/snapshots/interactive long command truncated.snap index 0e7fe656..18b8e18c 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select-truncate/snapshots/interactive long command truncated.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select-truncate/snapshots/interactive long command truncated.snap @@ -8,42 +8,42 @@ info: @ expect-milestone: task-select::0 Select a task (↑/↓, Enter to run, Esc to clear): - › build: echo build app - lint: echo lint app + › build: echo build app + lint: echo lint app long-cmd: echo aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa… - test: echo test app + test: echo test app @ write-key: down @ expect-milestone: task-select::1 Select a task (↑/↓, Enter to run, Esc to clear): - build: echo build app - › lint: echo lint app + build: echo build app + › lint: echo lint app long-cmd: echo aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa… - test: echo test app + test: echo test app @ write-key: down @ expect-milestone: task-select::2 Select a task (↑/↓, Enter to run, Esc to clear): - build: echo build app - lint: echo lint app + build: echo build app + lint: echo lint app › long-cmd: echo aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa… - test: echo test app + test: echo test app @ write-key: down @ expect-milestone: task-select::3 Select a task (↑/↓, Enter to run, Esc to clear): - build: echo build app - lint: echo lint app + build: echo build app + lint: echo lint app long-cmd: echo aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa… - › test: echo test app + › test: echo test app @ write-key: up @ expect-milestone: task-select::2 Select a task (↑/↓, Enter to run, Esc to clear): - build: echo build app - lint: echo lint app + build: echo build app + lint: echo lint app › long-cmd: echo aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa… - test: echo test app + test: echo test app @ write-key: enter Selected task: long-cmd ~/packages/app$ echo aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ⊘ cache disabled diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive enter with no results does nothing.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive enter with no results does nothing.snap index 99bca642..a9aa05b6 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive enter with no results does nothing.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive enter with no results does nothing.snap @@ -8,17 +8,17 @@ info: @ expect-milestone: task-select::0 Select a task (↑/↓, Enter to run, Esc to clear): - › build: echo build app - lint: echo lint app - test: echo test app - lib#build: echo build lib - lib#lint: echo lint lib - lib#test: echo test lib - lib#typecheck: echo typecheck lib - task-select-test#check: echo check root - task-select-test#clean: echo clean root + › build: echo build app + lint: echo lint app + test: echo test app + lib#build: echo build lib + lib#lint: echo lint lib + lib#test: echo test lib + lib#typecheck: echo typecheck lib + task-select-test#check: echo check root + task-select-test#clean: echo clean root task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root + task-select-test#docs: echo docs root task-select-test#format: echo format root (…3 more) @ write: zzzzz @@ -31,17 +31,17 @@ Select a task (↑/↓, Enter to run, Esc to clear): zzzzz @ expect-milestone: task-select::0 Select a task (↑/↓, Enter to run, Esc to clear): - › build: echo build app - lint: echo lint app - test: echo test app - lib#build: echo build lib - lib#lint: echo lint lib - lib#test: echo test lib - lib#typecheck: echo typecheck lib - task-select-test#check: echo check root - task-select-test#clean: echo clean root + › build: echo build app + lint: echo lint app + test: echo test app + lib#build: echo build lib + lib#lint: echo lint lib + lib#test: echo test lib + lib#typecheck: echo typecheck lib + task-select-test#check: echo check root + task-select-test#clean: echo clean root task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root + task-select-test#docs: echo docs root task-select-test#format: echo format root (…3 more) @ write-key: enter diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive escape clears query.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive escape clears query.snap index 505af762..b03e5fac 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive escape clears query.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive escape clears query.snap @@ -8,40 +8,40 @@ info: @ expect-milestone: task-select::0 Select a task (↑/↓, Enter to run, Esc to clear): - › build: echo build app - lint: echo lint app - test: echo test app - lib#build: echo build lib - lib#lint: echo lint lib - lib#test: echo test lib - lib#typecheck: echo typecheck lib - task-select-test#check: echo check root - task-select-test#clean: echo clean root + › build: echo build app + lint: echo lint app + test: echo test app + lib#build: echo build lib + lib#lint: echo lint lib + lib#test: echo test lib + lib#typecheck: echo typecheck lib + task-select-test#check: echo check root + task-select-test#clean: echo clean root task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root + task-select-test#docs: echo docs root task-select-test#format: echo format root (…3 more) @ write: lin @ expect-milestone: task-select:lin:0 Select a task (↑/↓, Enter to run, Esc to clear): lin - › lint: echo lint app + › lint: echo lint app lib#lint: echo lint lib @ write-key: escape @ expect-milestone: task-select::0 Select a task (↑/↓, Enter to run, Esc to clear): - › build: echo build app - lint: echo lint app - test: echo test app - lib#build: echo build lib - lib#lint: echo lint lib - lib#test: echo test lib - lib#typecheck: echo typecheck lib - task-select-test#check: echo check root - task-select-test#clean: echo clean root + › build: echo build app + lint: echo lint app + test: echo test app + lib#build: echo build lib + lib#lint: echo lint lib + lib#test: echo test lib + lib#typecheck: echo typecheck lib + task-select-test#check: echo check root + task-select-test#clean: echo clean root task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root + task-select-test#docs: echo docs root task-select-test#format: echo format root (…3 more) @ write-key: enter diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive scroll long list.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive scroll long list.snap index af033920..b58ef62f 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive scroll long list.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive scroll long list.snap @@ -8,17 +8,17 @@ info: @ expect-milestone: task-select::0 Select a task (↑/↓, Enter to run, Esc to clear): - › build: echo build app - lint: echo lint app - test: echo test app - lib#build: echo build lib - lib#lint: echo lint lib - lib#test: echo test lib - lib#typecheck: echo typecheck lib - task-select-test#check: echo check root - task-select-test#clean: echo clean root + › build: echo build app + lint: echo lint app + test: echo test app + lib#build: echo build lib + lib#lint: echo lint lib + lib#test: echo test lib + lib#typecheck: echo typecheck lib + task-select-test#check: echo check root + task-select-test#clean: echo clean root task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root + task-select-test#docs: echo docs root task-select-test#format: echo format root (…3 more) @ write-key: down @@ -32,17 +32,17 @@ Select a task (↑/↓, Enter to run, Esc to clear): @ expect-milestone: task-select::8 Select a task (↑/↓, Enter to run, Esc to clear): - build: echo build app - lint: echo lint app - test: echo test app - lib#build: echo build lib - lib#lint: echo lint lib - lib#test: echo test lib - lib#typecheck: echo typecheck lib - task-select-test#check: echo check root - › task-select-test#clean: echo clean root + build: echo build app + lint: echo lint app + test: echo test app + lib#build: echo build lib + lib#lint: echo lint lib + lib#test: echo test lib + lib#typecheck: echo typecheck lib + task-select-test#check: echo check root + › task-select-test#clean: echo clean root task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root + task-select-test#docs: echo docs root task-select-test#format: echo format root (…3 more) @ write-key: up @@ -56,17 +56,17 @@ Select a task (↑/↓, Enter to run, Esc to clear): @ expect-milestone: task-select::0 Select a task (↑/↓, Enter to run, Esc to clear): - › build: echo build app - lint: echo lint app - test: echo test app - lib#build: echo build lib - lib#lint: echo lint lib - lib#test: echo test lib - lib#typecheck: echo typecheck lib - task-select-test#check: echo check root - task-select-test#clean: echo clean root + › build: echo build app + lint: echo lint app + test: echo test app + lib#build: echo build lib + lib#lint: echo lint lib + lib#test: echo test lib + lib#typecheck: echo typecheck lib + task-select-test#check: echo check root + task-select-test#clean: echo clean root task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root + task-select-test#docs: echo docs root task-select-test#format: echo format root (…3 more) @ write-key: enter diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search other package task.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search other package task.snap index 23bc607c..8bb3a5a0 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search other package task.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search other package task.snap @@ -8,17 +8,17 @@ info: @ expect-milestone: task-select::0 Select a task (↑/↓, Enter to run, Esc to clear): - › build: echo build app - lint: echo lint app - test: echo test app - lib#build: echo build lib - lib#lint: echo lint lib - lib#test: echo test lib - lib#typecheck: echo typecheck lib - task-select-test#check: echo check root - task-select-test#clean: echo clean root + › build: echo build app + lint: echo lint app + test: echo test app + lib#build: echo build lib + lib#lint: echo lint lib + lib#test: echo test lib + lib#typecheck: echo typecheck lib + task-select-test#check: echo check root + task-select-test#clean: echo clean root task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root + task-select-test#docs: echo docs root task-select-test#format: echo format root (…3 more) @ write: typec diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search preserves rating within package.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search preserves rating within package.snap index 53b1d1d6..e51b79ad 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search preserves rating within package.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search preserves rating within package.snap @@ -8,35 +8,35 @@ info: @ expect-milestone: task-select::0 Select a task (↑/↓, Enter to run, Esc to clear): - › build: echo build lib - lint: echo lint lib - test: echo test lib - typecheck: echo typecheck lib - app#build: echo build app - app#lint: echo lint app - app#test: echo test app - task-select-test#check: echo check root - task-select-test#clean: echo clean root + › build: echo build lib + lint: echo lint lib + test: echo test lib + typecheck: echo typecheck lib + app#build: echo build app + app#lint: echo lint app + app#test: echo test app + task-select-test#check: echo check root + task-select-test#clean: echo clean root task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root + task-select-test#docs: echo docs root task-select-test#format: echo format root (…3 more) @ write: t @ expect-milestone: task-select:t:0 Select a task (↑/↓, Enter to run, Esc to clear): t - › test: echo test lib - typecheck: echo typecheck lib - lint: echo lint lib - task-select-test#check: echo check root - task-select-test#clean: echo clean root - task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root - task-select-test#format: echo format root - task-select-test#hello: echo hello from root + › test: echo test lib + typecheck: echo typecheck lib + lint: echo lint lib + task-select-test#check: echo check root + task-select-test#clean: echo clean root + task-select-test#deploy: echo deploy root + task-select-test#docs: echo docs root + task-select-test#format: echo format root + task-select-test#hello: echo hello from root task-select-test#run-typo-task: vp run nonexistent-xyz - task-select-test#validate: echo validate root - app#test: echo test app + task-select-test#validate: echo validate root + app#test: echo test app (…1 more) @ write-key: enter Selected task: test diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search then select.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search then select.snap index 754468a5..72cec2c3 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search then select.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search then select.snap @@ -8,24 +8,24 @@ info: @ expect-milestone: task-select::0 Select a task (↑/↓, Enter to run, Esc to clear): - › build: echo build app - lint: echo lint app - test: echo test app - lib#build: echo build lib - lib#lint: echo lint lib - lib#test: echo test lib - lib#typecheck: echo typecheck lib - task-select-test#check: echo check root - task-select-test#clean: echo clean root + › build: echo build app + lint: echo lint app + test: echo test app + lib#build: echo build lib + lib#lint: echo lint lib + lib#test: echo test lib + lib#typecheck: echo typecheck lib + task-select-test#check: echo check root + task-select-test#clean: echo clean root task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root + task-select-test#docs: echo docs root task-select-test#format: echo format root (…3 more) @ write: lin @ expect-milestone: task-select:lin:0 Select a task (↑/↓, Enter to run, Esc to clear): lin - › lint: echo lint app + › lint: echo lint app lib#lint: echo lint lib @ write-key: enter Selected task: lint diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search with hash skips reorder.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search with hash skips reorder.snap index e796bd6e..98f96fd7 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search with hash skips reorder.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search with hash skips reorder.snap @@ -8,26 +8,26 @@ info: @ expect-milestone: task-select::0 Select a task (↑/↓, Enter to run, Esc to clear): - › build: echo build app - lint: echo lint app - test: echo test app - lib#build: echo build lib - lib#lint: echo lint lib - lib#test: echo test lib - lib#typecheck: echo typecheck lib - task-select-test#check: echo check root - task-select-test#clean: echo clean root + › build: echo build app + lint: echo lint app + test: echo test app + lib#build: echo build lib + lib#lint: echo lint lib + lib#test: echo test lib + lib#typecheck: echo typecheck lib + task-select-test#check: echo check root + task-select-test#clean: echo clean root task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root + task-select-test#docs: echo docs root task-select-test#format: echo format root (…3 more) @ write: lib# @ expect-milestone: task-select:lib#:0 Select a task (↑/↓, Enter to run, Esc to clear): lib# - › lib#build: echo build lib - lib#lint: echo lint lib - lib#test: echo test lib + › lib#build: echo build lib + lib#lint: echo lint lib + lib#test: echo test lib lib#typecheck: echo typecheck lib @ write-key: enter Selected task: lib#build diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select task from lib.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select task from lib.snap index a1b0dc28..0da4f5f2 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select task from lib.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select task from lib.snap @@ -8,17 +8,17 @@ info: @ expect-milestone: task-select::0 Select a task (↑/↓, Enter to run, Esc to clear): - › build: echo build lib - lint: echo lint lib - test: echo test lib - typecheck: echo typecheck lib - app#build: echo build app - app#lint: echo lint app - app#test: echo test app - task-select-test#check: echo check root - task-select-test#clean: echo clean root + › build: echo build lib + lint: echo lint lib + test: echo test lib + typecheck: echo typecheck lib + app#build: echo build app + app#lint: echo lint app + app#test: echo test app + task-select-test#check: echo check root + task-select-test#clean: echo clean root task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root + task-select-test#docs: echo docs root task-select-test#format: echo format root (…3 more) @ write-key: enter diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select task.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select task.snap index d1a964e8..c86b5f6f 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select task.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select task.snap @@ -8,34 +8,34 @@ info: @ expect-milestone: task-select::0 Select a task (↑/↓, Enter to run, Esc to clear): - › build: echo build app - lint: echo lint app - test: echo test app - lib#build: echo build lib - lib#lint: echo lint lib - lib#test: echo test lib - lib#typecheck: echo typecheck lib - task-select-test#check: echo check root - task-select-test#clean: echo clean root + › build: echo build app + lint: echo lint app + test: echo test app + lib#build: echo build lib + lib#lint: echo lint lib + lib#test: echo test lib + lib#typecheck: echo typecheck lib + task-select-test#check: echo check root + task-select-test#clean: echo clean root task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root + task-select-test#docs: echo docs root task-select-test#format: echo format root (…3 more) @ write-key: down @ expect-milestone: task-select::1 Select a task (↑/↓, Enter to run, Esc to clear): - build: echo build app - › lint: echo lint app - test: echo test app - lib#build: echo build lib - lib#lint: echo lint lib - lib#test: echo test lib - lib#typecheck: echo typecheck lib - task-select-test#check: echo check root - task-select-test#clean: echo clean root + build: echo build app + › lint: echo lint app + test: echo test app + lib#build: echo build lib + lib#lint: echo lint lib + lib#test: echo test lib + lib#typecheck: echo typecheck lib + task-select-test#check: echo check root + task-select-test#clean: echo clean root task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root + task-select-test#docs: echo docs root task-select-test#format: echo format root (…3 more) @ write-key: enter diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select with typo.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select with typo.snap index cd51be84..d0a2150f 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select with typo.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select with typo.snap @@ -9,7 +9,7 @@ info: Task "buid" not found. Select a task (↑/↓, Enter to run, Esc to clear): buid - › build: echo build app + › build: echo build app lib#build: echo build lib @ write-key: enter Selected task: build diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/verbose with typo enters selector.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/verbose with typo enters selector.snap index 736f51e8..0ee80361 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/verbose with typo enters selector.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/verbose with typo enters selector.snap @@ -9,7 +9,7 @@ info: Task "buid" not found. Select a task (↑/↓, Enter to run, Esc to clear): buid - › build: echo build app + › build: echo build app lib#build: echo build lib @ write-key: enter Selected task: build diff --git a/crates/vite_workspace/src/package_graph.rs b/crates/vite_workspace/src/package_graph.rs index 24c66011..d74d5982 100644 --- a/crates/vite_workspace/src/package_graph.rs +++ b/crates/vite_workspace/src/package_graph.rs @@ -67,6 +67,19 @@ impl PackageQuery { pub(crate) const fn filters(filters: Vec1) -> Self { Self(PackageQueryKind::Filters(filters)) } + + /// Select the single package whose root is `path`. + /// + /// Used by the interactive task selector to match by filesystem path + /// instead of package name. + pub fn containing_package(path: Arc) -> Self { + Self(PackageQueryKind::Filters(Vec1::new(PackageFilter { + exclude: false, + selector: PackageSelector::ContainingPackage(path), + traversal: None, + source: None, + }))) + } } // ──────────────────────────────────────────────────────────────────────────── From b83caf56339779cdb707763b6993856813c3595c Mon Sep 17 00:00:00 2001 From: branchseer Date: Wed, 11 Mar 2026 15:30:34 +0800 Subject: [PATCH 5/9] feat: Add tree-view grouping to interactive task selector Group tasks by package in the interactive selector with non-selectable headers, and sort current-package items first in both interactive and non-interactive modes. Introduces DisplayRow as the single source of truth for the flattened display list, replacing the old before_render callback with built-in group_filtered logic in vite_select. Co-Authored-By: Claude Opus 4.6 --- crates/vite_select/src/interactive.rs | 638 ++++++++++++------ crates/vite_select/src/lib.rs | 36 +- crates/vite_task/src/session/mod.rs | 77 ++- .../task-list/snapshots/vp run in script.snap | 14 +- .../fixtures/task-select/snapshots.toml | 8 + ...ve enter with no results does nothing.snap | 52 +- .../interactive escape clears query.snap | 57 +- .../interactive scroll long list.snap | 78 +-- ...interactive search other package task.snap | 29 +- ...earch preserves rating within package.snap | 52 +- .../interactive search then select.snap | 31 +- ...active search with hash skips reorder.snap | 35 +- ...interactive select from other package.snap | 46 ++ .../interactive select task from lib.snap | 26 +- .../snapshots/interactive select task.snap | 52 +- .../interactive select with typo.snap | 5 +- .../verbose with typo enters selector.snap | 5 +- crates/vite_workspace/src/package_graph.rs | 1 + package.json | 3 +- packages/tools/package.json | 3 +- 20 files changed, 787 insertions(+), 461 deletions(-) create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select from other package.snap diff --git a/crates/vite_select/src/interactive.rs b/crates/vite_select/src/interactive.rs index 0eda3bc3..dcdfa127 100644 --- a/crates/vite_select/src/interactive.rs +++ b/crates/vite_select/src/interactive.rs @@ -3,12 +3,18 @@ use std::io::{Write, stdout}; use crossterm::{ cursor::{self, MoveToColumn}, event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}, - style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor}, + style::{Attribute, Color, SetAttribute, SetForegroundColor}, terminal::{self, Clear, ClearType}, }; +use vite_str::Str; use crate::{RenderState, SelectItem, fuzzy::fuzzy_match}; +/// Prefix width for root-level items (`" › "` or `" "`). +const ROOT_PREFIX_WIDTH: usize = 4; +/// Prefix width for grouped items (`" › "` or `" "`). +const GROUP_PREFIX_WIDTH: usize = 6; + struct RawModeGuard; impl RawModeGuard { @@ -24,19 +30,93 @@ impl Drop for RawModeGuard { } } +/// A row in the flattened display list. +pub enum DisplayRow { + /// Non-selectable group header line. + Header(Str), + /// Selectable item. `item_index` is the index into the original `items` slice. + Item { item_index: usize }, +} + +impl DisplayRow { + pub const fn is_item(&self) -> bool { + matches!(self, Self::Item { .. }) + } +} + +/// Filter, group, and flatten items into display rows. +/// +/// Pipeline: fuzzy match → group by `SelectItem::group` (current-package first) +/// → insert header rows at group boundaries. +pub fn build_display_rows(items: &[SelectItem], query: &str) -> Vec { + let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect(); + let mut filtered = fuzzy_match(query, &labels); + group_filtered(items, &mut filtered); + + let mut rows = Vec::new(); + let mut current_group: Option> = None; + for &item_idx in &filtered { + let group = items[item_idx].group.as_deref(); + if current_group != Some(group) { + current_group = Some(group); + if let Some(g) = group { + rows.push(DisplayRow::Header(Str::from(g))); + } + } + rows.push(DisplayRow::Item { item_index: item_idx }); + } + rows +} + +/// Reorder `filtered` so items are grouped by `SelectItem::group`. +/// +/// Groups are ordered by the position of their best-matching item in the +/// original fuzzy-scored `filtered` list. Items with `group: None` +/// (current-package tasks) are always placed first. +fn group_filtered(items: &[SelectItem], filtered: &mut Vec) { + // Collect group ordering: first-seen order preserves fuzzy rank. + let mut group_order: Vec> = Vec::new(); + for &idx in filtered.iter() { + let group = items[idx].group.as_deref(); + if !group_order.contains(&group) { + group_order.push(group); + } + } + // Always put current-package group (None) first. + if let Some(pos) = group_order.iter().position(Option::is_none) + && pos != 0 + { + let g = group_order.remove(pos); + group_order.insert(0, g); + } + let mut new_filtered = Vec::with_capacity(filtered.len()); + for &group in &group_order { + for &idx in filtered.iter() { + if items[idx].group.as_deref() == group { + new_filtered.push(idx); + } + } + } + *filtered = new_filtered; +} + struct State<'a> { items: &'a [SelectItem], - /// Indices into `items` that match the current query, in score order. - filtered: Vec, + /// Flattened display rows (headers + items): the single source of truth + /// after filtering + grouping. + display_rows: Vec, + /// Cached count of selectable items in `display_rows`. + item_count: usize, #[expect( clippy::disallowed_types, reason = "crossterm key events push chars one at a time; String is natural here" )] query: String, - /// Index into `filtered`. + /// Index among selectable items (0 = first Item row, 1 = second, etc.). selected: usize, - /// First visible row in the filtered list (scroll offset). + /// Index into `display_rows` — first visible row. scroll_offset: usize, + /// Max visible lines (display rows) in the viewport. page_size: usize, /// Number of lines rendered in the last frame (for clearing). rendered_lines: usize, @@ -45,77 +125,112 @@ struct State<'a> { impl<'a> State<'a> { fn new(items: &'a [SelectItem], initial_query: Option<&str>, page_size: usize) -> Self { let query = initial_query.unwrap_or_default().to_owned(); - let mut state = Self { + let display_rows = build_display_rows(items, &query); + let item_count = display_rows.iter().filter(|r| r.is_item()).count(); + Self { items, - filtered: Vec::new(), + display_rows, + item_count, query, selected: 0, scroll_offset: 0, page_size, rendered_lines: 0, - }; - state.refilter(); - state + } } fn refilter(&mut self) { - let labels: Vec<&str> = self.items.iter().map(|i| i.label.as_str()).collect(); - self.filtered = fuzzy_match(&self.query, &labels); + self.display_rows = build_display_rows(self.items, &self.query); + self.item_count = self.display_rows.iter().filter(|r| r.is_item()).count(); self.selected = 0; self.scroll_offset = 0; } - const fn move_up(&mut self) { - if self.selected > 0 { - self.selected -= 1; - if self.selected < self.scroll_offset { - self.scroll_offset = self.selected; + /// Find the display row index for the Nth selectable item. + fn display_row_of_selected(&self) -> Option { + let mut count = 0; + for (i, row) in self.display_rows.iter().enumerate() { + if row.is_item() { + if count == self.selected { + return Some(i); + } + count += 1; } } + None } - const fn move_down(&mut self) { - if !self.filtered.is_empty() && self.selected < self.filtered.len() - 1 { - self.selected += 1; - if self.selected >= self.scroll_offset + self.page_size { - self.scroll_offset = self.selected + 1 - self.page_size; - } + /// Get the original item index for the currently selected item. + fn selected_item_index(&self) -> Option { + let row_idx = self.display_row_of_selected()?; + match &self.display_rows[row_idx] { + DisplayRow::Item { item_index } => Some(*item_index), + DisplayRow::Header(_) => None, } } - fn selected_original_index(&self) -> Option { - self.filtered.get(self.selected).copied() + /// Ensure the selected item is within the visible viewport. + fn ensure_selected_visible(&mut self) { + let Some(row_idx) = self.display_row_of_selected() else { + self.scroll_offset = 0; + return; + }; + if row_idx < self.scroll_offset { + // If the selected item is a first item in a group, also show the header above it + self.scroll_offset = + if row_idx > 0 && matches!(self.display_rows[row_idx - 1], DisplayRow::Header(_)) { + row_idx - 1 + } else { + row_idx + }; + } else if row_idx >= self.scroll_offset + self.page_size { + self.scroll_offset = row_idx + 1 - self.page_size; + } } - fn visible_range(&self) -> std::ops::Range { - let end = (self.scroll_offset + self.page_size).min(self.filtered.len()); - self.scroll_offset..end + fn move_up(&mut self) { + if self.selected > 0 { + self.selected -= 1; + self.ensure_selected_visible(); + } } - const fn hidden_count(&self) -> usize { - self.filtered.len().saturating_sub(self.scroll_offset + self.page_size) + fn move_down(&mut self) { + if self.item_count > 0 && self.selected < self.item_count - 1 { + self.selected += 1; + self.ensure_selected_visible(); + } + } + + fn visible_display_rows(&self) -> std::ops::Range { + let end = (self.scroll_offset + self.page_size).min(self.display_rows.len()); + self.scroll_offset..end } -} -/// Split a label like `"package#task"` into `("package#", "task")`. -/// Labels without `#` return `("", label)`. -fn split_label(label: &str) -> (&str, &str) { - label.find('#').map_or(("", label), |pos| (&label[..=pos], &label[pos + 1..])) + /// Count selectable items (not headers) beyond the visible window. + fn hidden_item_count(&self) -> usize { + let visible_end = (self.scroll_offset + self.page_size).min(self.display_rows.len()); + self.display_rows[visible_end..].iter().filter(|r| r.is_item()).count() + } } /// Parameters for rendering a task list. pub struct RenderParams<'a> { pub items: &'a [SelectItem], - pub filtered: &'a [usize], - /// Index into `filtered` of the highlighted item, or `None` for non-interactive. - pub selected_in_filtered: Option, - /// Which slice of `filtered` to display. - pub visible_range: std::ops::Range, - /// Number of items beyond the visible range. + pub display_rows: &'a [DisplayRow], + /// Which selectable item is highlighted (0-based among Item rows), + /// or `None` for non-interactive. + pub selected: Option, + /// Which slice of `display_rows` to show. + pub visible_row_range: std::ops::Range, + /// Number of selectable items beyond the visible range. pub hidden_count: usize, pub header: Option<&'a str>, /// Current search text. `Some` enables the prompt line (interactive only). pub query: Option<&'a str>, + /// Whether to render group header lines. When `false`, items are still + /// grouped/sorted by `SelectItem::group` but headers are hidden. + pub show_group_headers: bool, /// `"\r\n"` for raw mode, `"\n"` for normal. pub line_ending: &'a str, /// Maximum visible width per line. Descriptions are truncated to prevent @@ -124,6 +239,19 @@ pub struct RenderParams<'a> { pub max_line_width: usize, } +/// Truncate a description string to fit within `max_chars`, appending ellipsis if needed. +fn truncate_desc<'a>(desc: &'a str, max_chars: usize, buf: &'a mut Str) -> &'a str { + let char_count = desc.chars().count(); + if char_count <= max_chars { + return desc; + } + let take = max_chars.saturating_sub(1); // room for "…" + #[expect(clippy::disallowed_types, reason = "intermediate collect for char truncation")] + let prefix: std::string::String = desc.chars().take(take).collect(); + *buf = vite_str::format!("{prefix}\u{2026}"); + buf.as_str() +} + /// Render the item list. Shared rendering logic used by both interactive /// and non-interactive modes (via [`crate::non_interactive`]). /// @@ -132,12 +260,13 @@ pub struct RenderParams<'a> { pub fn render_items(writer: &mut impl Write, params: &RenderParams<'_>) -> anyhow::Result { let RenderParams { items, - filtered, - selected_in_filtered, - visible_range, + display_rows, + selected, + visible_row_range, hidden_count, header, query, + show_group_headers, line_ending, max_line_width: _, } = params; @@ -171,81 +300,137 @@ pub fn render_items(writer: &mut impl Write, params: &RenderParams<'_>) -> anyho lines += 2; } - // Compute max label width for interactive column alignment - let max_label_width = if is_interactive { - visible_range.clone().map(|vi| items[filtered[vi]].label.chars().count()).max().unwrap_or(0) + // Single pre-pass: compute has_groups, max_name_width, has_items, and + // item_ordinal (count of Item rows before the visible range) together. + let mut has_groups = false; + let mut max_name_width = 0usize; + let mut has_items = false; + let mut item_ordinal = 0usize; + if is_interactive { + for (i, row) in display_rows.iter().enumerate() { + match row { + DisplayRow::Header(_) => has_groups = *show_group_headers, + DisplayRow::Item { item_index } => { + has_items = true; + let w = items[*item_index].display_name.chars().count(); + if w > max_name_width { + max_name_width = w; + } + if i < visible_row_range.start { + item_ordinal += 1; + } + } + } + } } else { - 0 - }; - - // Items - for vi in visible_range.clone() { - let item_idx = filtered[vi]; - let item = &items[item_idx]; - let is_selected = *selected_in_filtered == Some(vi); - - // Truncate description to prevent line wrapping. - // Line layout: - // - interactive prefix is " › " or " " (4 chars) - // - non-interactive prefix is " " (2 chars) - // then label (padded to max_label_width in interactive) + ": " + description - let label_width = item.label.chars().count(); - let padded_label_width = if is_interactive { max_label_width } else { label_width }; - let label_padding = padded_label_width - label_width; - let prefix_width = if is_interactive { 4 } else { 2 }; - let prefix_and_label_width = prefix_width + padded_label_width + 2; - let max_desc_chars = params.max_line_width.saturating_sub(prefix_and_label_width); - let desc_str = item.description.as_str(); - let desc_char_count = desc_str.chars().count(); - let truncated; - let display_desc = if desc_char_count > max_desc_chars { - let take = max_desc_chars.saturating_sub(1); // room for "…" - #[expect(clippy::disallowed_types, reason = "intermediate collect for char truncation")] - let prefix: std::string::String = desc_str.chars().take(take).collect(); - truncated = vite_str::format!("{prefix}\u{2026}"); - truncated.as_str() - } else { - desc_str - }; - - if is_interactive { - let (pkg, task) = split_label(&item.label); - if is_selected { - write!( - writer, - "{bold} \u{203a} {pkg}{task}:{:>pad$} {desc}{no_attr}{line_ending}", - "", - pad = label_padding, - bold = SetAttribute(Attribute::Bold), - no_attr = SetAttribute(Attribute::Reset), - desc = display_desc, - )?; - } else { - write!( - writer, - " {light_cyan}{pkg}{no_color}{cyan}{task}{no_attr}:{:>pad$} {desc}{line_ending}", - "", - pad = label_padding, - light_cyan = SetForegroundColor(Color::Rgb { r: 130, g: 200, b: 210 }), - cyan = SetForegroundColor(Color::Cyan), - no_color = ResetColor, - no_attr = SetAttribute(Attribute::Reset), - desc = display_desc, - )?; + has_items = display_rows.iter().any(DisplayRow::is_item); + } + + // Compute the absolute column where commands start (interactive only). + // All items — root and grouped — align their descriptions to the same column. + let max_prefix = if has_groups { GROUP_PREFIX_WIDTH } else { ROOT_PREFIX_WIDTH }; + // command_col = max_prefix + max_name_width + ": " + let command_col = if is_interactive { max_prefix + max_name_width + 2 } else { 0 }; + + // Render visible display rows + for ri in visible_row_range.clone() { + let row = &display_rows[ri]; + match row { + DisplayRow::Header(group_name) => { + if !show_group_headers { + continue; + } + if is_interactive { + write!( + writer, + " {dim}{name}{reset}{line_ending}", + dim = SetAttribute(Attribute::Dim), + name = group_name, + reset = SetAttribute(Attribute::Reset), + )?; + } else { + write!(writer, " {group_name}{line_ending}")?; + } + lines += 1; + } + DisplayRow::Item { item_index } => { + let item = &items[*item_index]; + let is_selected = *selected == Some(item_ordinal); + item_ordinal += 1; + let is_in_group = item.group.is_some(); + + let name = item.display_name.as_str(); + let name_width = name.chars().count(); + + let prefix_width = if is_interactive { + if is_in_group { GROUP_PREFIX_WIDTH } else { ROOT_PREFIX_WIDTH } + } else { + 2 + }; + + // Padding after colon to align all commands at `command_col`. + let name_padding = + if is_interactive { command_col - prefix_width - name_width - 2 } else { 0 }; + let max_desc_chars = params.max_line_width.saturating_sub(if is_interactive { + command_col + } else { + prefix_width + name_width + 2 + }); + + let mut truncated = Str::default(); + let display_desc = + truncate_desc(item.description.as_str(), max_desc_chars, &mut truncated); + + if is_interactive { + let (prefix, start_style, end_style) = if is_selected { + let p = if is_in_group { " \u{203a} " } else { " \u{203a} " }; + ( + p, + SetAttribute(Attribute::Bold).to_string(), + SetAttribute(Attribute::Reset).to_string(), + ) + } else { + let p = if is_in_group { " " } else { " " }; + #[expect( + clippy::disallowed_types, + reason = "building ANSI style string for crossterm formatting" + )] + let cyan: std::string::String = SetForegroundColor(Color::Cyan).to_string(); + (p, cyan, SetAttribute(Attribute::Reset).to_string()) + }; + if is_selected { + write!( + writer, + "{start_style}{prefix}{name}:{:>pad$} {desc}{end_style}{line_ending}", + "", + pad = name_padding, + desc = display_desc, + )?; + } else { + // Color only the name, not the colon/description. + write!( + writer, + "{prefix}{start_style}{name}{end_style}:{:>pad$} {desc}{line_ending}", + "", + pad = name_padding, + desc = display_desc, + )?; + } + } else if is_selected { + write!( + writer, + "{bold}> {name}: {desc}{reset}{line_ending}", + bold = SetAttribute(Attribute::Bold), + name = item.display_name, + desc = display_desc, + reset = SetAttribute(Attribute::Reset), + )?; + } else { + write!(writer, " {}: {display_desc}{line_ending}", item.display_name)?; + } + lines += 1; } - } else if is_selected { - write!( - writer, - "{bold}> {label}: {desc}{reset}{line_ending}", - bold = SetAttribute(Attribute::Bold), - label = item.label, - desc = display_desc, - reset = SetAttribute(Attribute::Reset), - )?; - } else { - write!(writer, " {}: {display_desc}{line_ending}", item.label)?; } - lines += 1; } // Footer: hidden items count @@ -255,7 +440,7 @@ pub fn render_items(writer: &mut impl Write, params: &RenderParams<'_>) -> anyho } // Empty state - if filtered.is_empty() { + if !has_items { write!(writer, " No matching tasks.{line_ending}")?; lines += 1; } @@ -288,12 +473,13 @@ fn render( stdout, &RenderParams { items: state.items, - filtered: &state.filtered, - selected_in_filtered: Some(state.selected), - visible_range: state.visible_range(), - hidden_count: state.hidden_count(), + display_rows: &state.display_rows, + selected: Some(state.selected), + visible_row_range: state.visible_display_rows(), + hidden_count: state.hidden_item_count(), header, query: Some(&state.query), + show_group_headers: true, line_ending: "\r\n", max_line_width, }, @@ -309,7 +495,6 @@ pub fn run( selected_index: &mut usize, header: Option<&str>, page_size: usize, - mut before_render: impl FnMut(&mut Vec, &str), mut after_render: impl FnMut(&RenderState<'_>), ) -> anyhow::Result<()> { if items.is_empty() { @@ -322,7 +507,6 @@ pub fn run( crossterm::execute!(out, cursor::Hide)?; let mut state = State::new(items, initial_query, page_size); - before_render(&mut state.filtered, &state.query); // Initial render render(&mut out, &mut state, header)?; @@ -336,14 +520,13 @@ pub fn run( // Clear the search query and reset the filter state.query.clear(); state.refilter(); - before_render(&mut state.filtered, &state.query); } KeyCode::Char('c') if modifiers.contains(KeyModifiers::CONTROL) => { cleanup(&mut out, &state)?; std::process::exit(130); } KeyCode::Enter => { - let Some(idx) = state.selected_original_index() else { + let Some(idx) = state.selected_item_index() else { continue; }; *selected_index = idx; @@ -359,12 +542,10 @@ pub fn run( KeyCode::Char(c) => { state.query.push(c); state.refilter(); - before_render(&mut state.filtered, &state.query); } KeyCode::Backspace => { state.query.pop(); state.refilter(); - before_render(&mut state.filtered, &state.query); } _ => continue, }, @@ -400,7 +581,25 @@ mod tests { fn make_items(items: &[(&str, &str)]) -> Vec { items .iter() - .map(|(label, desc)| SelectItem { label: (*label).into(), description: (*desc).into() }) + .map(|(label, desc)| SelectItem { + label: (*label).into(), + display_name: (*label).into(), + description: (*desc).into(), + group: None, + }) + .collect() + } + + /// Create items with explicit groups: (label, display_name, description, group) + fn make_grouped_items(items: &[(&str, &str, &str, Option<&str>)]) -> Vec { + items + .iter() + .map(|(label, display_name, desc, group)| SelectItem { + label: (*label).into(), + display_name: (*display_name).into(), + description: (*desc).into(), + group: group.map(|g| Str::from(g)), + }) .collect() } @@ -426,19 +625,20 @@ mod tests { #[expect(clippy::disallowed_types, reason = "test helper building arbitrary output string")] fn render_to_string(items: &[SelectItem], max_line_width: usize) -> String { - let filtered: Vec = (0..items.len()).collect(); - let len = filtered.len(); + let display_rows = build_display_rows(items, ""); + let len = display_rows.len(); let mut buf = Vec::new(); render_items( &mut buf, &RenderParams { items, - filtered: &filtered, - selected_in_filtered: Some(0), - visible_range: 0..len, + display_rows: &display_rows, + selected: Some(0), + visible_row_range: 0..len, hidden_count: 0, header: None, query: None, + show_group_headers: false, line_ending: "\n", max_line_width, }, @@ -453,19 +653,20 @@ mod tests { query: &str, max_line_width: usize, ) -> String { - let filtered: Vec = (0..items.len()).collect(); - let len = filtered.len(); + let display_rows = build_display_rows(items, query); + let len = display_rows.len(); let mut buf = Vec::new(); render_items( &mut buf, &RenderParams { items, - filtered: &filtered, - selected_in_filtered: Some(0), - visible_range: 0..len, + display_rows: &display_rows, + selected: Some(0), + visible_row_range: 0..len, hidden_count: 0, header: None, query: Some(query), + show_group_headers: true, line_ending: "\n", max_line_width, }, @@ -476,7 +677,7 @@ mod tests { #[test] fn truncates_long_description() { - let items = make_items(&[("build", "a]really long command that exceeds the width limit")]); + let items = make_items(&[("build", "a really long command that exceeds the width limit")]); // " build: a really long..." = 2 + 5 + 2 + desc // max_line_width = 30 => max_desc = 30 - 9 = 21 chars let output = render_to_string(&items, 30); @@ -546,9 +747,9 @@ mod tests { let spacer = lines.next().unwrap(); let selected = lines.next().unwrap(); let unselected = lines.next().unwrap(); - assert_eq!(prompt, "Select a task (↑/↓, Enter to run, Esc to clear):"); + assert_eq!(prompt, "Select a task (\u{2191}/\u{2193}, Enter to run, Esc to clear):"); assert!(spacer.is_empty()); - assert_eq!(selected, " › build: echo build"); + assert_eq!(selected, " \u{203a} build: echo build"); assert_eq!(unselected, " lint: echo lint"); } @@ -558,30 +759,20 @@ mod tests { make_items(&[("build", "echo build"), ("lint", "echo lint"), ("test", "vitest run")]); let output = render_interactive_to_string(&items, "", 80); let item_lines: Vec<&str> = output.lines().skip(2).collect(); - // max_label_width = 5 ("build") - // prefix(4) + max_label(5) + ":" + padding + " " + desc + // max_name_width = 5 ("build") + // prefix(4) + max_name(5) + ":" + padding + " " + desc assert_eq!(item_lines[0], " \u{203a} build: echo build"); assert_eq!(item_lines[1], " lint: echo lint"); assert_eq!(item_lines[2], " test: vitest run"); } - #[test] - fn interactive_alignment_with_package_labels() { - let items = make_items(&[("app#build", "echo build"), ("lint", "echo lint")]); - let output = render_interactive_to_string(&items, "", 80); - let item_lines: Vec<&str> = output.lines().skip(2).collect(); - // max_label_width = 9 ("app#build"), padding for "lint" = 5 - assert_eq!(item_lines[0], " \u{203a} app#build: echo build"); - assert_eq!(item_lines[1], " lint: echo lint"); - } - #[test] fn interactive_truncation_accounts_for_padding() { let items = make_items(&[ ("build", "a really long command that exceeds the width limit"), ("lint", "short"), ]); - // max_label_width = 5, prefix(4) + max_label(5) + sep(2) = 11 + // max_name_width = 5, prefix(4) + max_name(5) + sep(2) = 11 // max_line_width = 30 => max_desc = 30 - 11 = 19 chars let output = render_interactive_to_string(&items, "", 30); for line in output.lines().skip(2) { @@ -599,64 +790,113 @@ mod tests { } #[test] - fn interactive_padding_aligns_commands() { - let items = make_items(&[ - ("app#build", "echo build"), - ("app#lint", "echo lint"), - ("lib#typecheck", "echo tc"), + fn interactive_tree_view_with_groups() { + let items = make_grouped_items(&[ + ("build", "build", "echo build app", None), + ("lint", "lint", "echo lint app", None), + ("lib#build", "build", "echo build lib", Some("lib (packages/lib)")), + ("lib#lint", "lint", "echo lint lib", Some("lib (packages/lib)")), ]); let output = render_interactive_to_string(&items, "", 80); let item_lines: Vec<&str> = output.lines().skip(2).collect(); - // max_label_width = 13 ("lib#typecheck") - // Padding goes after ":" to align commands - assert_eq!(item_lines[0], " \u{203a} app#build: echo build"); - assert_eq!(item_lines[1], " app#lint: echo lint"); - assert_eq!(item_lines[2], " lib#typecheck: echo tc"); - - // Verify all commands start at the same char column - // prefix(4) + max_label(13) + ":" + padding + " " = commands start at column 19 - let cmd_columns: Vec = item_lines - .iter() - .map(|l| { - let colon_pos = l.chars().take_while(|&c| c != ':').count(); - // skip colon, then count padding spaces + the separator space - colon_pos - + 1 - + l[l.find(':').unwrap() + 1..].chars().take_while(|&c| c == ' ').count() - }) - .collect(); - assert!( - cmd_columns.windows(2).all(|w| w[0] == w[1]), - "command columns should be aligned: {cmd_columns:?}" - ); + // max_name=5, has_groups → max_prefix=6, command_col=13 + // Root items get extra padding to align with grouped items + assert_eq!(item_lines[0], " \u{203a} build: echo build app"); + assert_eq!(item_lines[1], " lint: echo lint app"); + // Group header + assert_eq!(item_lines[2], " lib (packages/lib)"); + // Grouped items (indented by 2 more, less padding) + assert_eq!(item_lines[3], " build: echo build lib"); + assert_eq!(item_lines[4], " lint: echo lint lib"); } #[test] - fn interactive_padding_with_truncation_preserves_ellipsis() { - let items = make_items(&[ - ("app#build", "a really long command that exceeds the width limit"), - ("lint", "short"), + fn interactive_tree_view_alignment_across_groups() { + let items = make_grouped_items(&[ + ("build", "build", "echo build", None), + ("typecheck", "typecheck", "echo tc", None), + ("lib#build", "build", "echo build lib", Some("lib")), + ]); + let output = render_interactive_to_string(&items, "", 80); + let item_lines: Vec<&str> = output.lines().skip(2).collect(); + // max_name=9, has_groups → max_prefix=6, command_col=17 + // All commands start at column 17 regardless of indent level + assert_eq!(item_lines[0], " \u{203a} build: echo build"); + assert_eq!(item_lines[1], " typecheck: echo tc"); + assert_eq!(item_lines[2], " lib"); + assert_eq!(item_lines[3], " build: echo build lib"); + } + + #[test] + fn interactive_tree_view_truncation_with_groups() { + let items = make_grouped_items(&[ + ("build", "build", "a really long command that exceeds the limit", None), + ( + "lib#build", + "build", + "another really long command that exceeds the limit", + Some("lib"), + ), ]); - // max_label_width = 9 ("app#build"), prefix(4) + 9 + sep(2) = 15 - // max_line_width = 30 => max_desc = 30 - 15 = 15 chars let output = render_interactive_to_string(&items, "", 30); for line in output.lines().skip(2) { - assert!( - line.chars().count() <= 30, - "line exceeds 30 chars: ({}) {line:?}", - line.chars().count() - ); + if !line.is_empty() { + assert!( + line.chars().count() <= 30, + "line exceeds 30 chars: ({}) {line:?}", + line.chars().count() + ); + } } - let build_line = output.lines().nth(2).unwrap(); + } + + #[test] + fn display_rows_built_correctly() { + let items = make_grouped_items(&[ + ("build", "build", "echo build", None), + ("lib#build", "build", "echo build lib", Some("lib")), + ("lib#lint", "lint", "echo lint lib", Some("lib")), + ("app#build", "build", "echo build app", Some("app")), + ]); + let rows = build_display_rows(&items, ""); + assert_eq!(rows.len(), 6); // 4 items + 2 headers ("lib", "app") + assert!(matches!(&rows[0], DisplayRow::Item { item_index: 0 })); + assert!(matches!(&rows[1], DisplayRow::Header(h) if h.as_str() == "lib")); + assert!(matches!(&rows[2], DisplayRow::Item { item_index: 1 })); + assert!(matches!(&rows[3], DisplayRow::Item { item_index: 2 })); + assert!(matches!(&rows[4], DisplayRow::Header(h) if h.as_str() == "app")); + assert!(matches!(&rows[5], DisplayRow::Item { item_index: 3 })); + } + + /// Mirrors the E2E scenario: items sorted alphabetically by package name + /// (app before lib), with lib being the current package (group: None). + /// Verifies that None-group items come first despite appearing later in input. + #[test] + fn display_rows_none_group_first_when_not_first_in_input() { + let items = make_grouped_items(&[ + // app items first (alphabetically before lib) + ("app#build", "app#build", "echo build app", Some("app (packages/app)")), + ("app#lint", "app#lint", "echo lint app", Some("app (packages/app)")), + // lib items (current package, group: None) + ("build", "build", "echo build lib", None), + ("lint", "lint", "echo lint lib", None), + // root items + ("root#check", "root#check", "echo check", Some("root (workspace root)")), + ]); + let rows = build_display_rows(&items, ""); + // None-group (lib) should come first, then app, then root assert!( - build_line.contains('\u{2026}'), - "truncated line should contain ellipsis: {build_line:?}" + matches!(&rows[0], DisplayRow::Item { item_index: 2 }), + "first item should be lib build (idx 2)" ); - // "lint" (4) has padding of 5 after colon (9 - 4) - let lint_line = output.lines().nth(3).unwrap(); assert!( - lint_line.contains("lint: short"), - "short label should have padding after colon: {lint_line:?}" + matches!(&rows[1], DisplayRow::Item { item_index: 3 }), + "second item should be lib lint (idx 3)" ); + assert!(matches!(&rows[2], DisplayRow::Header(h) if h.as_str() == "app (packages/app)")); + assert!(matches!(&rows[3], DisplayRow::Item { item_index: 0 })); + assert!(matches!(&rows[4], DisplayRow::Item { item_index: 1 })); + assert!(matches!(&rows[5], DisplayRow::Header(h) if h.as_str() == "root (workspace root)")); + assert!(matches!(&rows[6], DisplayRow::Item { item_index: 4 })); } } diff --git a/crates/vite_select/src/lib.rs b/crates/vite_select/src/lib.rs index 2082ce8c..228d737b 100644 --- a/crates/vite_select/src/lib.rs +++ b/crates/vite_select/src/lib.rs @@ -4,15 +4,20 @@ mod interactive; use std::io::Write; pub use fuzzy::fuzzy_match; -use interactive::{RenderParams, render_items}; +use interactive::{RenderParams, build_display_rows, render_items}; use vite_str::Str; /// An item in the selection list. pub struct SelectItem { - /// Display label, e.g. `"build"` or `"app#build"`. + /// Searchable label, e.g. `"build"` or `"app#build"`. Used for fuzzy matching. pub label: Str, - /// Description shown next to the label, e.g. `"echo build app"`. + /// Display name shown in the list, e.g. `"build"` (tree view) or `"app#build"` (flat). + pub display_name: Str, + /// Description shown next to the display name, e.g. `"echo build app"`. pub description: Str, + /// Group header text. Items sharing the same group render together under a + /// header line. `None` = top-level (no header). + pub group: Option, } /// Selection mode. @@ -63,7 +68,6 @@ pub fn select_list( writer: &mut impl Write, params: &SelectParams<'_>, mode: Mode<'_>, - before_render: impl FnMut(&mut Vec, &str), after_render: impl FnMut(&RenderState<'_>), ) -> anyhow::Result<()> { match mode { @@ -73,12 +77,9 @@ pub fn select_list( selected_index, params.header, params.page_size, - before_render, after_render, ), - Mode::NonInteractive => { - non_interactive(writer, params.items, params.query, params.header, before_render) - } + Mode::NonInteractive => non_interactive(writer, params.items, params.query, params.header), } } @@ -87,34 +88,33 @@ fn non_interactive( items: &[SelectItem], query: Option<&str>, header: Option<&str>, - mut before_render: impl FnMut(&mut Vec, &str), ) -> anyhow::Result<()> { - let labels: Vec<&str> = items.iter().map(|item| item.label.as_str()).collect(); - let mut filtered: Vec = - query.map_or_else(|| (0..items.len()).collect(), |q| fuzzy_match(q, &labels)); - before_render(&mut filtered, query.unwrap_or_default()); - let len = filtered.len(); + let display_rows = build_display_rows(items, query.unwrap_or_default()); // When there are no matching items, just print the header (if any) and // return early — avoids showing a redundant "No matching tasks." line // after a "not found" header. - if filtered.is_empty() { + let has_items = display_rows.iter().any(interactive::DisplayRow::is_item); + if !has_items { if let Some(h) = header { writeln!(writer, "{h}")?; } return Ok(()); } + let row_count = display_rows.len(); + render_items( writer, &RenderParams { items, - filtered: &filtered, - selected_in_filtered: None, - visible_range: 0..len, + display_rows: &display_rows, + selected: None, + visible_row_range: 0..row_count, hidden_count: 0, header, query: None, + show_group_headers: false, line_ending: "\n", max_line_width: usize::MAX, }, diff --git a/crates/vite_task/src/session/mod.rs b/crates/vite_task/src/session/mod.rs index e5b8d0e7..9b3f3a05 100644 --- a/crates/vite_task/src/session/mod.rs +++ b/crates/vite_task/src/session/mod.rs @@ -346,6 +346,10 @@ impl<'a> Session<'a> { clippy::future_not_send, reason = "session is single-threaded, futures do not need to be Send" )] + #[expect( + clippy::too_many_lines, + reason = "builds interactive/non-interactive select items and handles selection" + )] async fn handle_no_task( &mut self, is_interactive: bool, @@ -363,18 +367,48 @@ impl<'a> Session<'a> { .then_with(|| a.task_display.task_name.cmp(&b.task_display.task_name)) }); + let workspace_path = self.workspace_path(); + // Build items: current package tasks use unqualified names (no '#'), // other packages use qualified "package#task" names. + // Interactive mode uses tree view (grouped by package); non-interactive is flat. let select_items: Vec = entries .iter() .map(|entry| { - let label = - if current_package_path.as_ref() == Some(&entry.task_display.package_path) { - entry.task_display.task_name.clone() + let is_current = + current_package_path.as_ref() == Some(&entry.task_display.package_path); + let label = if is_current { + entry.task_display.task_name.clone() + } else { + vite_str::format!("{}", entry.task_display) + }; + + let group = if is_current { + None + } else { + let rel_path = entry + .task_display + .package_path + .strip_prefix(&*workspace_path) + .ok() + .flatten() + .map(|p| Str::from(p.as_str())) + .unwrap_or_default(); + let pkg_name = &entry.task_display.package_name; + let display_path = + if rel_path.is_empty() { Str::from("workspace root") } else { rel_path }; + Some(if pkg_name.is_empty() { + display_path } else { - vite_str::format!("{}", entry.task_display) - }; - SelectItem { label, description: entry.command.clone() } + vite_str::format!("{pkg_name} ({display_path})") + }) + }; + let display_name = if is_interactive { + entry.task_display.task_name.clone() + } else { + label.clone() + }; + SelectItem { label, display_name, description: entry.command.clone(), group } }) .collect(); @@ -410,28 +444,15 @@ impl<'a> Session<'a> { page_size: 12, }; - vite_select::select_list( - &mut stdout, - ¶ms, - mode, - |filtered, query| { - // When the query doesn't contain '#', move current-package tasks (those - // without '#' in their label) to the top. `sort_by_key` is a stable sort, - // so the fuzzy rating order is preserved within each group. - if !query.contains('#') { - filtered.sort_by_key(|&idx| select_items[idx].label.contains('#')); - } - }, - |state| { - use std::io::Write; - let milestone_name = - vite_str::format!("task-select:{}:{}", state.query, state.selected_index); - let milestone_bytes = pty_terminal_test_client::encoded_milestone(&milestone_name); - let mut out = std::io::stdout(); - let _ = out.write_all(&milestone_bytes); - let _ = out.flush(); - }, - )?; + vite_select::select_list(&mut stdout, ¶ms, mode, |state| { + use std::io::Write; + let milestone_name = + vite_str::format!("task-select:{}:{}", state.query, state.selected_index); + let milestone_bytes = pty_terminal_test_client::encoded_milestone(&milestone_name); + let mut out = std::io::stdout(); + let _ = out.write_all(&milestone_bytes); + let _ = out.flush(); + })?; let Some(selected_index) = selected_index else { // Non-interactive, the list was printed. diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-list/snapshots/vp run in script.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-list/snapshots/vp run in script.snap index 9202ca69..d317431f 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-list/snapshots/vp run in script.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-list/snapshots/vp run in script.snap @@ -7,12 +7,14 @@ expression: e2e_outputs $ vp run ⊘ cache disabled Select a task (↑/↓, Enter to run, Esc to clear): - › hello: echo hello from root - list-tasks: vp run - app#build: echo build app - app#lint: echo lint app - app#test: echo test app - lib#build: echo build lib + › hello: echo hello from root + list-tasks: vp run + app (packages/app) + build: echo build app + lint: echo lint app + test: echo test app + lib (packages/lib) + build: echo build lib @ write-key: enter $ vp run ⊘ cache disabled Selected task: hello diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots.toml b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots.toml index 5d553a8e..ef863e98 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots.toml +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots.toml @@ -132,6 +132,14 @@ steps = [ { command = "vp run", interactions = [{ "expect-milestone" = "task-select::0" }, { "write" = "zzzzz" }, { "expect-milestone" = "task-select:zzzzz:0" }, { "write-key" = "enter" }, { "write-key" = "escape" }, { "expect-milestone" = "task-select::0" }, { "write-key" = "enter" }] }, ] +# Interactive: navigate into a package group, select a non-current-package task +[[e2e]] +name = "interactive select from other package" +cwd = "packages/app" +steps = [ + { command = "vp run", interactions = [{ "expect-milestone" = "task-select::0" }, { "write-key" = "down" }, { "write-key" = "down" }, { "write-key" = "down" }, { "expect-milestone" = "task-select::3" }, { "write-key" = "enter" }] }, +] + # Typo inside a task script should fail with an error, NOT show a list [[e2e]] name = "typo in task script fails without list" diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive enter with no results does nothing.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive enter with no results does nothing.snap index a9aa05b6..7b92a3c6 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive enter with no results does nothing.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive enter with no results does nothing.snap @@ -8,19 +8,19 @@ info: @ expect-milestone: task-select::0 Select a task (↑/↓, Enter to run, Esc to clear): - › build: echo build app - lint: echo lint app - test: echo test app - lib#build: echo build lib - lib#lint: echo lint lib - lib#test: echo test lib - lib#typecheck: echo typecheck lib - task-select-test#check: echo check root - task-select-test#clean: echo clean root - task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root - task-select-test#format: echo format root - (…3 more) + › build: echo build app + lint: echo lint app + test: echo test app + lib (packages/lib) + build: echo build lib + lint: echo lint lib + test: echo test lib + typecheck: echo typecheck lib + task-select-test (workspace root) + check: echo check root + clean: echo clean root + deploy: echo deploy root + (…5 more) @ write: zzzzz @ expect-milestone: task-select:zzzzz:0 Select a task (↑/↓, Enter to run, Esc to clear): zzzzz @@ -31,19 +31,19 @@ Select a task (↑/↓, Enter to run, Esc to clear): zzzzz @ expect-milestone: task-select::0 Select a task (↑/↓, Enter to run, Esc to clear): - › build: echo build app - lint: echo lint app - test: echo test app - lib#build: echo build lib - lib#lint: echo lint lib - lib#test: echo test lib - lib#typecheck: echo typecheck lib - task-select-test#check: echo check root - task-select-test#clean: echo clean root - task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root - task-select-test#format: echo format root - (…3 more) + › build: echo build app + lint: echo lint app + test: echo test app + lib (packages/lib) + build: echo build lib + lint: echo lint lib + test: echo test lib + typecheck: echo typecheck lib + task-select-test (workspace root) + check: echo check root + clean: echo clean root + deploy: echo deploy root + (…5 more) @ write-key: enter Selected task: build ~/packages/app$ echo build app ⊘ cache disabled diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive escape clears query.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive escape clears query.snap index b03e5fac..f80a88e3 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive escape clears query.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive escape clears query.snap @@ -8,42 +8,43 @@ info: @ expect-milestone: task-select::0 Select a task (↑/↓, Enter to run, Esc to clear): - › build: echo build app - lint: echo lint app - test: echo test app - lib#build: echo build lib - lib#lint: echo lint lib - lib#test: echo test lib - lib#typecheck: echo typecheck lib - task-select-test#check: echo check root - task-select-test#clean: echo clean root - task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root - task-select-test#format: echo format root - (…3 more) + › build: echo build app + lint: echo lint app + test: echo test app + lib (packages/lib) + build: echo build lib + lint: echo lint lib + test: echo test lib + typecheck: echo typecheck lib + task-select-test (workspace root) + check: echo check root + clean: echo clean root + deploy: echo deploy root + (…5 more) @ write: lin @ expect-milestone: task-select:lin:0 Select a task (↑/↓, Enter to run, Esc to clear): lin - › lint: echo lint app - lib#lint: echo lint lib + › lint: echo lint app + lib (packages/lib) + lint: echo lint lib @ write-key: escape @ expect-milestone: task-select::0 Select a task (↑/↓, Enter to run, Esc to clear): - › build: echo build app - lint: echo lint app - test: echo test app - lib#build: echo build lib - lib#lint: echo lint lib - lib#test: echo test lib - lib#typecheck: echo typecheck lib - task-select-test#check: echo check root - task-select-test#clean: echo clean root - task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root - task-select-test#format: echo format root - (…3 more) + › build: echo build app + lint: echo lint app + test: echo test app + lib (packages/lib) + build: echo build lib + lint: echo lint lib + test: echo test lib + typecheck: echo typecheck lib + task-select-test (workspace root) + check: echo check root + clean: echo clean root + deploy: echo deploy root + (…5 more) @ write-key: enter Selected task: build ~/packages/app$ echo build app ⊘ cache disabled diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive scroll long list.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive scroll long list.snap index b58ef62f..b9835651 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive scroll long list.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive scroll long list.snap @@ -8,19 +8,19 @@ info: @ expect-milestone: task-select::0 Select a task (↑/↓, Enter to run, Esc to clear): - › build: echo build app - lint: echo lint app - test: echo test app - lib#build: echo build lib - lib#lint: echo lint lib - lib#test: echo test lib - lib#typecheck: echo typecheck lib - task-select-test#check: echo check root - task-select-test#clean: echo clean root - task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root - task-select-test#format: echo format root - (…3 more) + › build: echo build app + lint: echo lint app + test: echo test app + lib (packages/lib) + build: echo build lib + lint: echo lint lib + test: echo test lib + typecheck: echo typecheck lib + task-select-test (workspace root) + check: echo check root + clean: echo clean root + deploy: echo deploy root + (…5 more) @ write-key: down @ write-key: down @ write-key: down @@ -32,19 +32,19 @@ Select a task (↑/↓, Enter to run, Esc to clear): @ expect-milestone: task-select::8 Select a task (↑/↓, Enter to run, Esc to clear): - build: echo build app - lint: echo lint app - test: echo test app - lib#build: echo build lib - lib#lint: echo lint lib - lib#test: echo test lib - lib#typecheck: echo typecheck lib - task-select-test#check: echo check root - › task-select-test#clean: echo clean root - task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root - task-select-test#format: echo format root - (…3 more) + build: echo build app + lint: echo lint app + test: echo test app + lib (packages/lib) + build: echo build lib + lint: echo lint lib + test: echo test lib + typecheck: echo typecheck lib + task-select-test (workspace root) + check: echo check root + › clean: echo clean root + deploy: echo deploy root + (…5 more) @ write-key: up @ write-key: up @ write-key: up @@ -56,19 +56,19 @@ Select a task (↑/↓, Enter to run, Esc to clear): @ expect-milestone: task-select::0 Select a task (↑/↓, Enter to run, Esc to clear): - › build: echo build app - lint: echo lint app - test: echo test app - lib#build: echo build lib - lib#lint: echo lint lib - lib#test: echo test lib - lib#typecheck: echo typecheck lib - task-select-test#check: echo check root - task-select-test#clean: echo clean root - task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root - task-select-test#format: echo format root - (…3 more) + › build: echo build app + lint: echo lint app + test: echo test app + lib (packages/lib) + build: echo build lib + lint: echo lint lib + test: echo test lib + typecheck: echo typecheck lib + task-select-test (workspace root) + check: echo check root + clean: echo clean root + deploy: echo deploy root + (…5 more) @ write-key: enter Selected task: build ~/packages/app$ echo build app ⊘ cache disabled diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search other package task.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search other package task.snap index 8bb3a5a0..e2e24041 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search other package task.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search other package task.snap @@ -8,24 +8,25 @@ info: @ expect-milestone: task-select::0 Select a task (↑/↓, Enter to run, Esc to clear): - › build: echo build app - lint: echo lint app - test: echo test app - lib#build: echo build lib - lib#lint: echo lint lib - lib#test: echo test lib - lib#typecheck: echo typecheck lib - task-select-test#check: echo check root - task-select-test#clean: echo clean root - task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root - task-select-test#format: echo format root - (…3 more) + › build: echo build app + lint: echo lint app + test: echo test app + lib (packages/lib) + build: echo build lib + lint: echo lint lib + test: echo test lib + typecheck: echo typecheck lib + task-select-test (workspace root) + check: echo check root + clean: echo clean root + deploy: echo deploy root + (…5 more) @ write: typec @ expect-milestone: task-select:typec:0 Select a task (↑/↓, Enter to run, Esc to clear): typec - › lib#typecheck: echo typecheck lib + lib (packages/lib) + › typecheck: echo typecheck lib @ write-key: enter Selected task: lib#typecheck ~/packages/lib$ echo typecheck lib ⊘ cache disabled diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search preserves rating within package.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search preserves rating within package.snap index e51b79ad..35bbd984 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search preserves rating within package.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search preserves rating within package.snap @@ -8,36 +8,36 @@ info: @ expect-milestone: task-select::0 Select a task (↑/↓, Enter to run, Esc to clear): - › build: echo build lib - lint: echo lint lib - test: echo test lib - typecheck: echo typecheck lib - app#build: echo build app - app#lint: echo lint app - app#test: echo test app - task-select-test#check: echo check root - task-select-test#clean: echo clean root - task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root - task-select-test#format: echo format root - (…3 more) + › build: echo build lib + lint: echo lint lib + test: echo test lib + typecheck: echo typecheck lib + app (packages/app) + build: echo build app + lint: echo lint app + test: echo test app + task-select-test (workspace root) + check: echo check root + clean: echo clean root + deploy: echo deploy root + (…5 more) @ write: t @ expect-milestone: task-select:t:0 Select a task (↑/↓, Enter to run, Esc to clear): t - › test: echo test lib - typecheck: echo typecheck lib - lint: echo lint lib - task-select-test#check: echo check root - task-select-test#clean: echo clean root - task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root - task-select-test#format: echo format root - task-select-test#hello: echo hello from root - task-select-test#run-typo-task: vp run nonexistent-xyz - task-select-test#validate: echo validate root - app#test: echo test app - (…1 more) + › test: echo test lib + typecheck: echo typecheck lib + lint: echo lint lib + task-select-test (workspace root) + check: echo check root + clean: echo clean root + deploy: echo deploy root + docs: echo docs root + format: echo format root + hello: echo hello from root + run-typo-task: vp run nonexistent-xyz + validate: echo validate root + (…2 more) @ write-key: enter Selected task: test ~/packages/lib$ echo test lib ⊘ cache disabled diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search then select.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search then select.snap index 72cec2c3..b130cbbe 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search then select.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search then select.snap @@ -8,25 +8,26 @@ info: @ expect-milestone: task-select::0 Select a task (↑/↓, Enter to run, Esc to clear): - › build: echo build app - lint: echo lint app - test: echo test app - lib#build: echo build lib - lib#lint: echo lint lib - lib#test: echo test lib - lib#typecheck: echo typecheck lib - task-select-test#check: echo check root - task-select-test#clean: echo clean root - task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root - task-select-test#format: echo format root - (…3 more) + › build: echo build app + lint: echo lint app + test: echo test app + lib (packages/lib) + build: echo build lib + lint: echo lint lib + test: echo test lib + typecheck: echo typecheck lib + task-select-test (workspace root) + check: echo check root + clean: echo clean root + deploy: echo deploy root + (…5 more) @ write: lin @ expect-milestone: task-select:lin:0 Select a task (↑/↓, Enter to run, Esc to clear): lin - › lint: echo lint app - lib#lint: echo lint lib + › lint: echo lint app + lib (packages/lib) + lint: echo lint lib @ write-key: enter Selected task: lint ~/packages/app$ echo lint app ⊘ cache disabled diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search with hash skips reorder.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search with hash skips reorder.snap index 98f96fd7..0b18b0b7 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search with hash skips reorder.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search with hash skips reorder.snap @@ -8,27 +8,28 @@ info: @ expect-milestone: task-select::0 Select a task (↑/↓, Enter to run, Esc to clear): - › build: echo build app - lint: echo lint app - test: echo test app - lib#build: echo build lib - lib#lint: echo lint lib - lib#test: echo test lib - lib#typecheck: echo typecheck lib - task-select-test#check: echo check root - task-select-test#clean: echo clean root - task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root - task-select-test#format: echo format root - (…3 more) + › build: echo build app + lint: echo lint app + test: echo test app + lib (packages/lib) + build: echo build lib + lint: echo lint lib + test: echo test lib + typecheck: echo typecheck lib + task-select-test (workspace root) + check: echo check root + clean: echo clean root + deploy: echo deploy root + (…5 more) @ write: lib# @ expect-milestone: task-select:lib#:0 Select a task (↑/↓, Enter to run, Esc to clear): lib# - › lib#build: echo build lib - lib#lint: echo lint lib - lib#test: echo test lib - lib#typecheck: echo typecheck lib + lib (packages/lib) + › build: echo build lib + lint: echo lint lib + test: echo test lib + typecheck: echo typecheck lib @ write-key: enter Selected task: lib#build ~/packages/lib$ echo build lib ⊘ cache disabled diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select from other package.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select from other package.snap new file mode 100644 index 00000000..57b88930 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select from other package.snap @@ -0,0 +1,46 @@ +--- +source: crates/vite_task_bin/tests/e2e_snapshots/main.rs +expression: e2e_outputs +info: + cwd: packages/app +--- +> vp run +@ expect-milestone: task-select::0 +Select a task (↑/↓, Enter to run, Esc to clear): + + › build: echo build app + lint: echo lint app + test: echo test app + lib (packages/lib) + build: echo build lib + lint: echo lint lib + test: echo test lib + typecheck: echo typecheck lib + task-select-test (workspace root) + check: echo check root + clean: echo clean root + deploy: echo deploy root + (…5 more) +@ write-key: down +@ write-key: down +@ write-key: down +@ expect-milestone: task-select::3 +Select a task (↑/↓, Enter to run, Esc to clear): + + build: echo build app + lint: echo lint app + test: echo test app + lib (packages/lib) + › build: echo build lib + lint: echo lint lib + test: echo test lib + typecheck: echo typecheck lib + task-select-test (workspace root) + check: echo check root + clean: echo clean root + deploy: echo deploy root + (…5 more) +@ write-key: enter +Selected task: lib#build +~/packages/lib$ echo build lib ⊘ cache disabled +build lib diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select task from lib.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select task from lib.snap index 0da4f5f2..b665f058 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select task from lib.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select task from lib.snap @@ -8,19 +8,19 @@ info: @ expect-milestone: task-select::0 Select a task (↑/↓, Enter to run, Esc to clear): - › build: echo build lib - lint: echo lint lib - test: echo test lib - typecheck: echo typecheck lib - app#build: echo build app - app#lint: echo lint app - app#test: echo test app - task-select-test#check: echo check root - task-select-test#clean: echo clean root - task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root - task-select-test#format: echo format root - (…3 more) + › build: echo build lib + lint: echo lint lib + test: echo test lib + typecheck: echo typecheck lib + app (packages/app) + build: echo build app + lint: echo lint app + test: echo test app + task-select-test (workspace root) + check: echo check root + clean: echo clean root + deploy: echo deploy root + (…5 more) @ write-key: enter Selected task: build ~/packages/lib$ echo build lib ⊘ cache disabled diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select task.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select task.snap index c86b5f6f..a44a45d2 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select task.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select task.snap @@ -8,36 +8,36 @@ info: @ expect-milestone: task-select::0 Select a task (↑/↓, Enter to run, Esc to clear): - › build: echo build app - lint: echo lint app - test: echo test app - lib#build: echo build lib - lib#lint: echo lint lib - lib#test: echo test lib - lib#typecheck: echo typecheck lib - task-select-test#check: echo check root - task-select-test#clean: echo clean root - task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root - task-select-test#format: echo format root - (…3 more) + › build: echo build app + lint: echo lint app + test: echo test app + lib (packages/lib) + build: echo build lib + lint: echo lint lib + test: echo test lib + typecheck: echo typecheck lib + task-select-test (workspace root) + check: echo check root + clean: echo clean root + deploy: echo deploy root + (…5 more) @ write-key: down @ expect-milestone: task-select::1 Select a task (↑/↓, Enter to run, Esc to clear): - build: echo build app - › lint: echo lint app - test: echo test app - lib#build: echo build lib - lib#lint: echo lint lib - lib#test: echo test lib - lib#typecheck: echo typecheck lib - task-select-test#check: echo check root - task-select-test#clean: echo clean root - task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root - task-select-test#format: echo format root - (…3 more) + build: echo build app + › lint: echo lint app + test: echo test app + lib (packages/lib) + build: echo build lib + lint: echo lint lib + test: echo test lib + typecheck: echo typecheck lib + task-select-test (workspace root) + check: echo check root + clean: echo clean root + deploy: echo deploy root + (…5 more) @ write-key: enter Selected task: lint ~/packages/app$ echo lint app ⊘ cache disabled diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select with typo.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select with typo.snap index d0a2150f..bbecae61 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select with typo.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select with typo.snap @@ -9,8 +9,9 @@ info: Task "buid" not found. Select a task (↑/↓, Enter to run, Esc to clear): buid - › build: echo build app - lib#build: echo build lib + › build: echo build app + lib (packages/lib) + build: echo build lib @ write-key: enter Selected task: build ~/packages/app$ echo build app ⊘ cache disabled diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/verbose with typo enters selector.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/verbose with typo enters selector.snap index 0ee80361..0cdb961a 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/verbose with typo enters selector.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/verbose with typo enters selector.snap @@ -9,8 +9,9 @@ info: Task "buid" not found. Select a task (↑/↓, Enter to run, Esc to clear): buid - › build: echo build app - lib#build: echo build lib + › build: echo build app + lib (packages/lib) + build: echo build lib @ write-key: enter Selected task: build ~/packages/app$ echo build app ⊘ cache disabled diff --git a/crates/vite_workspace/src/package_graph.rs b/crates/vite_workspace/src/package_graph.rs index d74d5982..69908d62 100644 --- a/crates/vite_workspace/src/package_graph.rs +++ b/crates/vite_workspace/src/package_graph.rs @@ -72,6 +72,7 @@ impl PackageQuery { /// /// Used by the interactive task selector to match by filesystem path /// instead of package name. + #[must_use] pub fn containing_package(path: Arc) -> Self { Self(PackageQueryKind::Filters(Vec1::new(PackageFilter { exclude: false, diff --git a/package.json b/package.json index 07c6453c..d2f709d2 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "type": "module", "scripts": { "prepare": "husky", - "build": "echo build" + "build": "echo build", + "build2": "echo build" }, "devDependencies": { "@types/node": "catalog:", diff --git a/packages/tools/package.json b/packages/tools/package.json index 9bf26bff..27004ab3 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -13,7 +13,8 @@ }, "scripts": { "build": "echo foo", - "lint": "vp lint" + "lint": "vp lint", + "test": "echo test" }, "dependencies": { "cross-env": "^10.1.0", From 439bc6951e0fc557c29428124544b41279cc7be5 Mon Sep 17 00:00:00 2001 From: branchseer Date: Wed, 11 Mar 2026 15:36:10 +0800 Subject: [PATCH 6/9] refactor: Avoid per-item String allocations for ANSI style codes in render loop Use crossterm's Display-implementing types directly in write! instead of calling .to_string(), eliminating 2-3 heap allocations per visible item per render frame. Also simplify prefix selection with a match expression. Co-Authored-By: Claude Opus 4.6 --- crates/vite_select/src/interactive.rs | 31 +++++++++++---------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/crates/vite_select/src/interactive.rs b/crates/vite_select/src/interactive.rs index dcdfa127..a1eefd5a 100644 --- a/crates/vite_select/src/interactive.rs +++ b/crates/vite_select/src/interactive.rs @@ -382,48 +382,41 @@ pub fn render_items(writer: &mut impl Write, params: &RenderParams<'_>) -> anyho truncate_desc(item.description.as_str(), max_desc_chars, &mut truncated); if is_interactive { - let (prefix, start_style, end_style) = if is_selected { - let p = if is_in_group { " \u{203a} " } else { " \u{203a} " }; - ( - p, - SetAttribute(Attribute::Bold).to_string(), - SetAttribute(Attribute::Reset).to_string(), - ) - } else { - let p = if is_in_group { " " } else { " " }; - #[expect( - clippy::disallowed_types, - reason = "building ANSI style string for crossterm formatting" - )] - let cyan: std::string::String = SetForegroundColor(Color::Cyan).to_string(); - (p, cyan, SetAttribute(Attribute::Reset).to_string()) + let prefix = match (is_selected, is_in_group) { + (true, true) => " \u{203a} ", + (true, false) => " \u{203a} ", + (false, true) => " ", + (false, false) => " ", }; + let reset = SetAttribute(Attribute::Reset); if is_selected { + let bold = SetAttribute(Attribute::Bold); write!( writer, - "{start_style}{prefix}{name}:{:>pad$} {desc}{end_style}{line_ending}", + "{bold}{prefix}{name}:{:>pad$} {desc}{reset}{line_ending}", "", pad = name_padding, desc = display_desc, )?; } else { // Color only the name, not the colon/description. + let cyan = SetForegroundColor(Color::Cyan); write!( writer, - "{prefix}{start_style}{name}{end_style}:{:>pad$} {desc}{line_ending}", + "{prefix}{cyan}{name}{reset}:{:>pad$} {desc}{line_ending}", "", pad = name_padding, desc = display_desc, )?; } } else if is_selected { + let bold = SetAttribute(Attribute::Bold); + let reset = SetAttribute(Attribute::Reset); write!( writer, "{bold}> {name}: {desc}{reset}{line_ending}", - bold = SetAttribute(Attribute::Bold), name = item.display_name, desc = display_desc, - reset = SetAttribute(Attribute::Reset), )?; } else { write!(writer, " {}: {display_desc}{line_ending}", item.display_name)?; From 7ab588b37f1c58b75e288e1ff388cf253c8d1906 Mon Sep 17 00:00:00 2001 From: branchseer Date: Wed, 11 Mar 2026 15:46:50 +0800 Subject: [PATCH 7/9] feat: Match task selector colors with vite-plus command picker Selected items: DarkGrey marker, Blue+Bold label, DarkGrey description. Non-selected items: default label color, DarkGrey description. Co-Authored-By: Claude Opus 4.6 --- crates/vite_select/src/interactive.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/vite_select/src/interactive.rs b/crates/vite_select/src/interactive.rs index a1eefd5a..5b467a27 100644 --- a/crates/vite_select/src/interactive.rs +++ b/crates/vite_select/src/interactive.rs @@ -389,21 +389,21 @@ pub fn render_items(writer: &mut impl Write, params: &RenderParams<'_>) -> anyho (false, false) => " ", }; let reset = SetAttribute(Attribute::Reset); + let dark_grey = SetForegroundColor(Color::DarkGrey); if is_selected { + let blue = SetForegroundColor(Color::Blue); let bold = SetAttribute(Attribute::Bold); write!( writer, - "{bold}{prefix}{name}:{:>pad$} {desc}{reset}{line_ending}", + "{dark_grey}{prefix}{reset}{blue}{bold}{name}:{reset}{dark_grey}{:>pad$} {desc}{reset}{line_ending}", "", pad = name_padding, desc = display_desc, )?; } else { - // Color only the name, not the colon/description. - let cyan = SetForegroundColor(Color::Cyan); write!( writer, - "{prefix}{cyan}{name}{reset}:{:>pad$} {desc}{line_ending}", + "{prefix}{name}:{dark_grey}{:>pad$} {desc}{reset}{line_ending}", "", pad = name_padding, desc = display_desc, From 4c0c35f9c296827077c42051cdc6ea677707fbc6 Mon Sep 17 00:00:00 2001 From: branchseer Date: Wed, 11 Mar 2026 15:50:31 +0800 Subject: [PATCH 8/9] fix: Resolve clippy warnings in task selector Co-Authored-By: Claude Opus 4.6 --- crates/vite_select/src/interactive.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/vite_select/src/interactive.rs b/crates/vite_select/src/interactive.rs index 5b467a27..b56c6350 100644 --- a/crates/vite_select/src/interactive.rs +++ b/crates/vite_select/src/interactive.rs @@ -583,7 +583,7 @@ mod tests { .collect() } - /// Create items with explicit groups: (label, display_name, description, group) + /// Create items with explicit groups: (label, `display_name`, description, group) fn make_grouped_items(items: &[(&str, &str, &str, Option<&str>)]) -> Vec { items .iter() @@ -591,7 +591,7 @@ mod tests { label: (*label).into(), display_name: (*display_name).into(), description: (*desc).into(), - group: group.map(|g| Str::from(g)), + group: group.map(Str::from), }) .collect() } From b330347a1e74276229a71dd991b43cb4682810b8 Mon Sep 17 00:00:00 2001 From: branchseer Date: Wed, 11 Mar 2026 15:53:17 +0800 Subject: [PATCH 9/9] chore: Remove temporary test scripts from package.json files Co-Authored-By: Claude Opus 4.6 --- package.json | 4 +--- packages/tools/package.json | 5 ----- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/package.json b/package.json index d2f709d2..07146397 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,7 @@ }, "type": "module", "scripts": { - "prepare": "husky", - "build": "echo build", - "build2": "echo build" + "prepare": "husky" }, "devDependencies": { "@types/node": "catalog:", diff --git a/packages/tools/package.json b/packages/tools/package.json index 27004ab3..769b10ef 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -11,11 +11,6 @@ "read-stdin": "./src/read-stdin.js", "replace-file-content": "./src/replace-file-content.ts" }, - "scripts": { - "build": "echo foo", - "lint": "vp lint", - "test": "echo test" - }, "dependencies": { "cross-env": "^10.1.0", "oxfmt": "0.26.0",