Skip to content

Commit 261c567

Browse files
branchseerclaude
andauthored
feat: improve task runner summary with compact one-liners (#171)
## Summary Replace the verbose full execution summary with compact one-liners for normal runs, with `--verbose` flag for full summary and `--last-details` to view the last run's saved summary from a JSON file. ### Behavior | Command | Behavior | |---|---| | `vp run task` | Run task, **compact** summary | | `vp run -v task` | Run task, **full** summary | | `vp run --last-details` | Read saved JSON, display full summary | ### Compact summary rules - Single task + not cache hit → no summary at all - Single task + cache hit → `[vp run] cache hit, {duration} saved.` - Multi-task → `[vp run] {hits}/{total} cache hit ({rate}%), {duration} saved. (Run 'vp run --verbose' for full details)` ### Key design decisions - **Structured data, not formatted strings**: all display formatting happens at render time from `LastRunSummary` - **Enums for invalid states**: `--last-details` is exclusive at the type level via `Command::RunLastDetails` variant - **Single rendering path**: always generate `LastRunSummary`, then render compact or full from it - **Atomic writes**: `last-summary.json` written to `.tmp` then `fs::rename` - **Write callback**: reporter decoupled from file paths via `Box<dyn FnOnce(&LastRunSummary)>` - **Direct TaskSummary construction**: leaf reporter builds `TaskSummary` in `finish()` instead of collecting intermediate `ExecutionInfo` ### Snapshot proof Includes a commit adding `--verbose` to all e2e tests followed by its revert, proving the full summary output is correct and unchanged. The e2e tests exercise the compact summary path by default. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3d1554b commit 261c567

File tree

87 files changed

+1526
-1913
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

87 files changed

+1526
-1913
lines changed

crates/vite_task/src/cli/mod.rs

Lines changed: 90 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub enum CacheSubcommand {
1717
/// Extracted as a separate struct so they can be cheaply `Copy`-ed
1818
/// before `RunCommand` is consumed.
1919
#[derive(Debug, Clone, Copy, clap::Args)]
20+
#[expect(clippy::struct_excessive_bools, reason = "CLI flags are naturally boolean")]
2021
pub struct RunFlags {
2122
/// Run tasks found in all packages in the workspace, in topological order based on package dependencies.
2223
#[clap(default_value = "false", short, long)]
@@ -29,23 +30,43 @@ pub struct RunFlags {
2930
/// Do not run dependencies specified in `dependsOn` fields.
3031
#[clap(default_value = "false", long)]
3132
pub ignore_depends_on: bool,
33+
34+
/// Show full detailed summary after execution.
35+
#[clap(default_value = "false", short = 'v', long)]
36+
pub verbose: bool,
3237
}
3338

34-
/// Arguments for the `run` subcommand.
39+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
40+
// Public CLI types (clap-parsed)
41+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
42+
43+
/// Arguments for the `run` subcommand as parsed by clap.
44+
///
45+
/// Contains the `--last-details` flag which is resolved into a separate
46+
/// `ResolvedCommand::RunLastDetails` variant internally.
3547
#[derive(Debug, clap::Args)]
3648
pub struct RunCommand {
3749
/// `packageName#taskName` or `taskName`. If omitted, lists all available tasks.
38-
pub task_specifier: Option<TaskSpecifier>,
50+
pub(crate) task_specifier: Option<TaskSpecifier>,
3951

4052
#[clap(flatten)]
41-
pub flags: RunFlags,
53+
pub(crate) flags: RunFlags,
4254

4355
/// Additional arguments to pass to the tasks
4456
#[clap(trailing_var_arg = true, allow_hyphen_values = true)]
45-
pub additional_args: Vec<Str>,
57+
pub(crate) additional_args: Vec<Str>,
58+
59+
/// Display the detailed summary of the last run.
60+
#[clap(long, exclusive = true)]
61+
pub(crate) last_details: bool,
4662
}
4763

48-
/// vite task CLI subcommands
64+
/// vite task CLI subcommands as parsed by clap.
65+
///
66+
/// vite task CLI subcommands as parsed by clap.
67+
///
68+
/// Pass directly to `Session::main` or `HandledCommand::ViteTaskCommand`.
69+
/// The `--last-details` flag on the `run` subcommand is resolved internally.
4970
#[derive(Debug, Parser)]
5071
pub enum Command {
5172
/// Run tasks
@@ -57,6 +78,68 @@ pub enum Command {
5778
},
5879
}
5980

81+
impl Command {
82+
/// Resolve the clap-parsed command into the dispatched [`ResolvedCommand`] enum.
83+
///
84+
/// When `--last-details` is set on the `run` subcommand, this produces
85+
/// [`ResolvedCommand::RunLastDetails`] instead of [`ResolvedCommand::Run`],
86+
/// making the exclusivity enforced at the type level.
87+
#[must_use]
88+
pub(crate) fn into_resolved(self) -> ResolvedCommand {
89+
match self {
90+
Self::Run(run) if run.last_details => ResolvedCommand::RunLastDetails,
91+
Self::Run(run) => ResolvedCommand::Run(run.into_resolved()),
92+
Self::Cache { subcmd } => ResolvedCommand::Cache { subcmd },
93+
}
94+
}
95+
}
96+
97+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
98+
// Internal resolved types (used for dispatch — `--last-details` is a separate variant)
99+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
100+
101+
/// Resolved CLI command for internal dispatch.
102+
///
103+
/// Unlike [`Command`], this enum makes `--last-details` a separate variant
104+
/// ([`ResolvedCommand::RunLastDetails`]) so that it is exclusive at the type level —
105+
/// there is no way to combine it with task execution fields.
106+
#[derive(Debug)]
107+
pub enum ResolvedCommand {
108+
/// Run tasks with the given parameters.
109+
Run(ResolvedRunCommand),
110+
/// Display the saved detailed summary of the last run (`--last-details`).
111+
RunLastDetails,
112+
/// Manage the task cache.
113+
Cache { subcmd: CacheSubcommand },
114+
}
115+
116+
/// Resolved arguments for executing tasks.
117+
///
118+
/// Does not contain `last_details` — that case is represented by
119+
/// [`ResolvedCommand::RunLastDetails`] instead.
120+
#[derive(Debug)]
121+
pub struct ResolvedRunCommand {
122+
/// `packageName#taskName` or `taskName`. If omitted, lists all available tasks.
123+
pub task_specifier: Option<TaskSpecifier>,
124+
125+
pub flags: RunFlags,
126+
127+
/// Additional arguments to pass to the tasks.
128+
pub additional_args: Vec<Str>,
129+
}
130+
131+
impl RunCommand {
132+
/// Convert to the resolved run command, stripping the `last_details` flag.
133+
#[must_use]
134+
pub(crate) fn into_resolved(self) -> ResolvedRunCommand {
135+
ResolvedRunCommand {
136+
task_specifier: self.task_specifier,
137+
flags: self.flags,
138+
additional_args: self.additional_args,
139+
}
140+
}
141+
}
142+
60143
#[derive(thiserror::Error, Debug)]
61144
pub enum CLITaskQueryError {
62145
#[error("no task specifier provided")]
@@ -69,7 +152,7 @@ pub enum CLITaskQueryError {
69152
PackageNameSpecifiedWithRecursive { package_name: Str, task_name: Str },
70153
}
71154

72-
impl RunCommand {
155+
impl ResolvedRunCommand {
73156
/// Convert to `QueryPlanRequest`, or return an error if invalid.
74157
///
75158
/// # Errors
@@ -82,7 +165,7 @@ impl RunCommand {
82165
) -> Result<QueryPlanRequest, CLITaskQueryError> {
83166
let Self {
84167
task_specifier,
85-
flags: RunFlags { recursive, transitive, ignore_depends_on },
168+
flags: RunFlags { recursive, transitive, ignore_depends_on, .. },
86169
additional_args,
87170
} = self;
88171

crates/vite_task/src/session/cache/display.rs

Lines changed: 42 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@
44
//! Coloring is handled by the reporter to respect `NO_COLOR` environment variable.
55
66
use rustc_hash::FxHashSet;
7+
use serde::{Deserialize, Serialize};
78
use vite_str::Str;
89
use vite_task_plan::cache_metadata::SpawnFingerprint;
910

1011
use super::{CacheMiss, FingerprintMismatch};
11-
use crate::session::event::{CacheDisabledReason, CacheStatus};
12+
use crate::session::event::CacheStatus;
1213

13-
/// Describes a single atomic change between two spawn fingerprints
14-
enum SpawnFingerprintChange {
14+
/// Describes a single atomic change between two spawn fingerprints.
15+
///
16+
/// Used both for live cache status display and for persisted summary data.
17+
#[derive(Serialize, Deserialize)]
18+
pub enum SpawnFingerprintChange {
1519
// Environment variable changes
1620
/// Environment variable added
1721
EnvAdded { key: Str, value: Str },
@@ -43,8 +47,40 @@ enum SpawnFingerprintChange {
4347
FingerprintIgnoreRemoved { pattern: Str },
4448
}
4549

46-
/// Compare two spawn fingerprints and return all changes
47-
fn detect_spawn_fingerprint_changes(
50+
/// Format a single spawn fingerprint change as human-readable text.
51+
///
52+
/// Used by both the live cache status display and the persisted summary rendering.
53+
pub fn format_spawn_change(change: &SpawnFingerprintChange) -> Str {
54+
match change {
55+
SpawnFingerprintChange::EnvAdded { key, value } => {
56+
vite_str::format!("env {key}={value} added")
57+
}
58+
SpawnFingerprintChange::EnvRemoved { key, value } => {
59+
vite_str::format!("env {key}={value} removed")
60+
}
61+
SpawnFingerprintChange::EnvValueChanged { key, old_value, new_value } => {
62+
vite_str::format!("env {key} value changed from '{old_value}' to '{new_value}'")
63+
}
64+
SpawnFingerprintChange::PassThroughEnvAdded { name } => {
65+
vite_str::format!("pass-through env '{name}' added")
66+
}
67+
SpawnFingerprintChange::PassThroughEnvRemoved { name } => {
68+
vite_str::format!("pass-through env '{name}' removed")
69+
}
70+
SpawnFingerprintChange::ProgramChanged => Str::from("program changed"),
71+
SpawnFingerprintChange::ArgsChanged => Str::from("args changed"),
72+
SpawnFingerprintChange::CwdChanged => Str::from("working directory changed"),
73+
SpawnFingerprintChange::FingerprintIgnoreAdded { pattern } => {
74+
vite_str::format!("fingerprint ignore '{pattern}' added")
75+
}
76+
SpawnFingerprintChange::FingerprintIgnoreRemoved { pattern } => {
77+
vite_str::format!("fingerprint ignore '{pattern}' removed")
78+
}
79+
}
80+
}
81+
82+
/// Compare two spawn fingerprints and return all changes.
83+
pub fn detect_spawn_fingerprint_changes(
4884
old: &SpawnFingerprint,
4985
new: &SpawnFingerprint,
5086
) -> Vec<SpawnFingerprintChange> {
@@ -180,105 +216,6 @@ pub fn format_cache_status_inline(cache_status: &CacheStatus) -> Option<Str> {
180216
};
181217
Some(vite_str::format!("✗ cache miss: {reason}, executing"))
182218
}
183-
CacheStatus::Disabled(reason) => {
184-
// Show inline message for disabled cache
185-
let message = match reason {
186-
CacheDisabledReason::InProcessExecution => "cache disabled: built-in command",
187-
CacheDisabledReason::NoCacheMetadata => "cache disabled: no cache config",
188-
};
189-
Some(vite_str::format!("⊘ {message}"))
190-
}
191-
}
192-
}
193-
194-
/// Format cache status for summary display (post-execution).
195-
///
196-
/// Returns a formatted string showing detailed cache information.
197-
/// - Cache Hit: Shows saved time
198-
/// - Cache Miss (NotFound): Indicates first-time execution
199-
/// - Cache Miss (with mismatch): Shows specific reason with details
200-
/// - Cache Disabled: Shows user-friendly reason message
201-
///
202-
/// Note: Returns plain text without styling. The reporter applies colors.
203-
pub fn format_cache_status_summary(cache_status: &CacheStatus) -> Str {
204-
match cache_status {
205-
CacheStatus::Hit { replayed_duration } => {
206-
// Show saved time for cache hits
207-
vite_str::format!("→ Cache hit - output replayed - {replayed_duration:.2?} saved")
208-
}
209-
CacheStatus::Miss(CacheMiss::NotFound) => {
210-
// First time running this task - no previous cache entry
211-
Str::from("→ Cache miss: no previous cache entry found")
212-
}
213-
CacheStatus::Miss(CacheMiss::FingerprintMismatch(mismatch)) => {
214-
// Show specific reason why cache was invalidated
215-
match mismatch {
216-
FingerprintMismatch::SpawnFingerprintMismatch { old, new } => {
217-
let changes = detect_spawn_fingerprint_changes(old, new);
218-
let formatted: Vec<Str> = changes
219-
.iter()
220-
.map(|c| match c {
221-
SpawnFingerprintChange::EnvAdded { key, value } => {
222-
vite_str::format!("env {key}={value} added")
223-
}
224-
SpawnFingerprintChange::EnvRemoved { key, value } => {
225-
vite_str::format!("env {key}={value} removed")
226-
}
227-
SpawnFingerprintChange::EnvValueChanged {
228-
key,
229-
old_value,
230-
new_value,
231-
} => {
232-
vite_str::format!(
233-
"env {key} value changed from '{old_value}' to '{new_value}'"
234-
)
235-
}
236-
SpawnFingerprintChange::PassThroughEnvAdded { name } => {
237-
vite_str::format!("pass-through env '{name}' added")
238-
}
239-
SpawnFingerprintChange::PassThroughEnvRemoved { name } => {
240-
vite_str::format!("pass-through env '{name}' removed")
241-
}
242-
SpawnFingerprintChange::ProgramChanged => Str::from("program changed"),
243-
SpawnFingerprintChange::ArgsChanged => Str::from("args changed"),
244-
SpawnFingerprintChange::CwdChanged => {
245-
Str::from("working directory changed")
246-
}
247-
SpawnFingerprintChange::FingerprintIgnoreAdded { pattern } => {
248-
vite_str::format!("fingerprint ignore '{pattern}' added")
249-
}
250-
SpawnFingerprintChange::FingerprintIgnoreRemoved { pattern } => {
251-
vite_str::format!("fingerprint ignore '{pattern}' removed")
252-
}
253-
})
254-
.collect();
255-
256-
if formatted.is_empty() {
257-
Str::from("→ Cache miss: configuration changed")
258-
} else {
259-
let joined =
260-
formatted.iter().map(Str::as_str).collect::<Vec<_>>().join("; ");
261-
vite_str::format!("→ Cache miss: {joined}")
262-
}
263-
}
264-
FingerprintMismatch::PostRunFingerprintMismatch(diff) => {
265-
// Post-run mismatch has specific path information
266-
use crate::session::execute::fingerprint::PostRunFingerprintMismatch;
267-
match diff {
268-
PostRunFingerprintMismatch::InputContentChanged { path } => {
269-
vite_str::format!("→ Cache miss: content of input '{path}' changed")
270-
}
271-
}
272-
}
273-
}
274-
}
275-
CacheStatus::Disabled(reason) => {
276-
// Display user-friendly message for each disabled reason
277-
let message = match reason {
278-
CacheDisabledReason::InProcessExecution => "Cache disabled for built-in command",
279-
CacheDisabledReason::NoCacheMetadata => "Cache disabled in task configuration",
280-
};
281-
vite_str::format!("→ {message}")
282-
}
219+
CacheStatus::Disabled(_) => Some(Str::from("⊘ cache disabled")),
283220
}
284221
}

crates/vite_task/src/session/cache/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ use std::{fmt::Display, fs::File, io::Write, sync::Arc, time::Duration};
66

77
use bincode::{Decode, Encode, decode_from_slice, encode_to_vec};
88
// Re-export display functions for convenience
9-
pub use display::{format_cache_status_inline, format_cache_status_summary};
9+
pub use display::format_cache_status_inline;
10+
pub use display::{SpawnFingerprintChange, detect_spawn_fingerprint_changes, format_spawn_change};
1011
use rusqlite::{Connection, OptionalExtension as _, config::DbConfig};
1112
use serde::{Deserialize, Serialize};
1213
use tokio::sync::Mutex;

0 commit comments

Comments
 (0)