feat(cache): store colored task logs, strip at display when needed#378
Conversation
Spawn cached tasks with FORCE_COLOR=1 so cached output is always colored. Color-related env vars (NO_COLOR, COLORTERM, TERM, TERM_PROGRAM) are no longer passed through by default; FORCE_COLOR is pre-inserted with value "1" before pattern filtering. The reporter wraps its output writers with `anstream::StripStream` when the user's terminal doesn't support colors, so colored cache replays display correctly on plain terminals. Color support is now decided once at the CLI binary level via `supports_color::on(Stream::Stdout)` and threaded into the reporter builders as a `bool` parameter; the reporter no longer reads NO_COLOR itself. New `vtt print-color` helper and `color_env_handling` e2e fixture exercise the colour pipeline end-to-end, with a new opt-in `formatted-snapshot` mode on the e2e harness (via vt100's `Screen::contents_formatted`) for asserting ANSI bytes in cached replays. https://claude.ai/code/session_01EAhpw6TzUWExMeF7hdwuF3
Replace the hand-rolled escape table in `render_formatted_screen` with a straight pass through `std::ascii::escape_default`, keeping only the newline as a special case so snapshots remain multi-line in markdown. https://claude.ai/code/session_01EAhpw6TzUWExMeF7hdwuF3
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 92de5b89db
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
`Screen::contents_formatted` emits the full terminal state including cursor positioning (`\x1b[5;1H`), trailing `\r` and cursor-visibility sequences. The exact bytes depended on platform-specific PTY/runner behavior (Linux had `\x1b[5;1H`, macOS had `\x1b[4;1H`, Windows collapsed differently), making the new color e2e snapshot flaky. Switch `screen_contents_formatted` to `Screen::rows_formatted`, which yields per-row content with SGR attribute escapes only — no cursor moves, no screen erase. Trailing empty rows are dropped and the remaining rows are joined with `\n`. The e2e renderer then just feeds the bytes through `std::ascii::escape_default`; the hand-rolled SGR filter is gone. https://claude.ai/code/session_01EAhpw6TzUWExMeF7hdwuF3
Windows ConPTY rewrites the byte stream that vt100 sees, eliding bare `\x1b[m` resets between styled spans and at the end of styled runs. Unix PTYs keep them, so the same screen state produced different formatted bytes per platform. Strip `\x1b[m` from each `rows_formatted` row in `screen_contents_formatted`. The non-reset SGR transitions (`\x1b[34m`, `\x1b[32m`, etc.) — which are the bytes the test actually asserts on — survive intact on every platform. https://claude.ai/code/session_01EAhpw6TzUWExMeF7hdwuF3
Reporters previously took a single `color_support: bool` derived from
stdout. When stdout is redirected (non-TTY) but stderr is still a TTY,
this incorrectly stripped colour from stderr too.
Replace the bool with a `ColorSupport { stdout, stderr }` struct and
plumb it through every reporter builder. The reporter's own writes
(command lines, summaries) continue to use the stdout flag, and each
pipe writer in [`InterleavedLeafReporter`]/[`LabeledLeafReporter`]/
[`PlainReporter`] now picks the flag matching its stream. The grouped
reporter funnels everything into a single buffer flushed via the main
writer, so it keeps using the stdout flag.
[`Session`] detects each stream with its own
`supports_color::on(Stream::*)` cache. Also bumps the Windows-only plan
snapshot that stored `NO_COLOR: 1` (now `FORCE_COLOR: 1` after the
default untracked-env list dropped the colour vars).
https://claude.ai/code/session_01EAhpw6TzUWExMeF7hdwuF3
`ColorizeExt::style` used to short-circuit to a `Style::new()` when `NO_COLOR` was set; with stripping now handled at the writer layer the trait reduced to a pure passthrough to `OwoColorize::style`. Use `OwoColorize::style` directly at the call sites. https://claude.ai/code/session_01EAhpw6TzUWExMeF7hdwuF3
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8ae0cec795
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
`show_last_run_details` formats the saved summary via `format_full_summary` and used to rely on the `ColorizeExt` wrapper to silently drop styles when `NO_COLOR` was set. After the wrapper was removed, the formatted buffer always carried ANSI escapes, so users with `NO_COLOR=1` or a redirected stdout would see escape codes in the saved-summary output. Route the buffer through `maybe_strip_writer(stdout_supports_color())` so the strip layer runs in the same way as the live reporter path. https://claude.ai/code/session_01EAhpw6TzUWExMeF7hdwuF3
Restrict `maybe_strip_writer` to the `StdioConfig.writers` returned from `LeafExecutionReporter::start` — those receive bytes the runner does not control (child stdout/stderr, cache replay) so stripping at the writer level is the only correct place. For the reporter's own writes (command banners, error lines, summary blocks, --last-details rendering) introduce a thin `ColorizeExt::style` extension that delegates to `OwoColorize::if_supports_color(Stream::Stdout, …)`. Call sites keep the existing `.style(Style::new().bold())` syntax; the result is now a no-op on terminals that lack colour support (honouring `NO_COLOR`, `FORCE_COLOR`, and TTY detection via the `supports-color` crate). For the grouped reporter, child output is buffered before display, so its pipe writers wrap `GroupedWriter` with `maybe_strip_writer` and the main writer is stored unwrapped. Plain/Interleaved/Labeled/Summary builders likewise no longer wrap their main writer. https://claude.ai/code/session_01EAhpw6TzUWExMeF7hdwuF3
Doc-link `[\`Self::start\`]` resolves against the builder type (which has no `start` method) and `[\`ColorizeExt\`]` is out of scope in `summary_reporter.rs`, so `cargo doc -D warnings` rejected the new comments. Demote both to plain code-fenced names — they still read clearly without the rustdoc cross-link. https://claude.ai/code/session_01EAhpw6TzUWExMeF7hdwuF3
`[\`Self::finish\`]` on `GroupedReporterBuilder` resolves against the builder struct (which has no `finish` method), so `cargo doc -D warnings` rejected it. Replaced with prose. Re-ran `RUSTDOCFLAGS=-D warnings cargo doc --workspace --no-deps` and the workspace docs build cleanly. https://claude.ai/code/session_01EAhpw6TzUWExMeF7hdwuF3
Bumps the vite-task git dependency from `88bacaa` to `c63db22`. ## Notable upstream changes - feat(cache): add `output` globs for cache restoration ([vite-task#375](voidzero-dev/vite-task#375)) - feat(cache): store colored task logs, strip at display when needed ([vite-task#378](voidzero-dev/vite-task#378)) - fix(plan): move FORCE_COLOR fallback after pattern filtering ([vite-task#379](voidzero-dev/vite-task#379)) - fix: preserve `PATHEXT` for Windows cached tasks ([vite-task#366](voidzero-dev/vite-task#366)) ## Vite+ side changes - Added the new `output: None` field to all `EnabledCacheConfig` initializers in `packages/cli/binding/src/cli/{handler,resolver}.rs` to match the new field added in vite-task#375. - Bumped `rusqlite` to `0.39.0` to match vite-task's new requirement and resolve a `libsqlite3-sys` links conflict. - Regenerated `packages/cli/src/run-config.ts` types snapshot (vite-task task map type tightened). - Removed the `pass-no-color-env` snap test, which is no longer relevant now that `NO_COLOR` is not in vite-task's default env passthrough. - Updated `docs/config/run.md`: - Added an `output` cache config section. - Refreshed the default-passthrough terminal env vars list (only `FORCE_COLOR` is auto-passed; other color vars need opt-in). Compare: voidzero-dev/vite-task@88bacaa...c63db22#diff-06572a96a58dc510037d5efa622f9bec8519bc1beab13c9f251e97e657a9d4ed --------- Co-authored-by: Claude <noreply@anthropic.com>
Bumps the vite-task git dependency from `88bacaa` to `c63db22`. ## Notable upstream changes - feat(cache): add `output` globs for cache restoration ([vite-task#375](voidzero-dev/vite-task#375)) - feat(cache): store colored task logs, strip at display when needed ([vite-task#378](voidzero-dev/vite-task#378)) - fix(plan): move FORCE_COLOR fallback after pattern filtering ([vite-task#379](voidzero-dev/vite-task#379)) - fix: preserve `PATHEXT` for Windows cached tasks ([vite-task#366](voidzero-dev/vite-task#366)) ## Vite+ side changes - Added the new `output: None` field to all `EnabledCacheConfig` initializers in `packages/cli/binding/src/cli/{handler,resolver}.rs` to match the new field added in vite-task#375. - Bumped `rusqlite` to `0.39.0` to match vite-task's new requirement and resolve a `libsqlite3-sys` links conflict. - Regenerated `packages/cli/src/run-config.ts` types snapshot (vite-task task map type tightened). - Removed the `pass-no-color-env` snap test, which is no longer relevant now that `NO_COLOR` is not in vite-task's default env passthrough. - Updated `docs/config/run.md`: - Added an `output` cache config section. - Refreshed the default-passthrough terminal env vars list (only `FORCE_COLOR` is auto-passed; other color vars need opt-in). Compare: voidzero-dev/vite-task@88bacaa...c63db22#diff-06572a96a58dc510037d5efa622f9bec8519bc1beab13c9f251e97e657a9d4ed --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #358.
Summary
FORCE_COLOR=1, so the cache always stores task logs with ANSI colors intact. On a cache hit the original bytes are replayed verbatim — cached logs keep their colors regardless of the parent'sFORCE_COLORvalue.supports-colorcrate.NO_COLOR,COLORTERM,TERM, andTERM_PROGRAMare no longer inDEFAULT_UNTRACKED_ENV. Tasks that need them can opt in viaenv/untrackedEnv.Implementation notes
ColorSupport { stdout, stderr }struct threads per-stream support through the reporter builders.maybe_strip_writeris applied only to theStdioConfig.writersreturned fromLeafExecutionReporter::start(child stdout/stderr and the grouped buffer); banners and summaries use a thinColorizeExt::styleextension that delegates toOwoColorize::if_supports_color.formatted-snapshot = true(per-step) and switchedscreen_contents_formattedtovt100::Screen::rows_formatted. Raw\x1b[mresets are stripped from the rendered rows so snapshots match across Linux/macOS/Windows ConPTY.Test plan
cargo test -p vite_task -p vite_task_plan -p vite_task_graphcargo test -p vite_task_bin --test e2e_snapshotscargo clippy --testson Linuxcargo doc --workspace -D warningshttps://claude.ai/code/session_01EAhpw6TzUWExMeF7hdwuF3