Skip to content

feat(cache): store colored task logs, strip at display when needed#378

Merged
branchseer merged 16 commits into
mainfrom
claude/color-env-handling-gzWtn
May 12, 2026
Merged

feat(cache): store colored task logs, strip at display when needed#378
branchseer merged 16 commits into
mainfrom
claude/color-env-handling-gzWtn

Conversation

@branchseer
Copy link
Copy Markdown
Member

@branchseer branchseer commented May 12, 2026

Fixes #358.

Summary

  • Cached tasks are spawned with 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's FORCE_COLOR value.
  • Colors are stripped at display time when the user's terminal does not support them, decided per stdout/stderr via the supports-color crate.
  • NO_COLOR, COLORTERM, TERM, and TERM_PROGRAM are no longer in DEFAULT_UNTRACKED_ENV. Tasks that need them can opt in via env / untrackedEnv.

Implementation notes

  • A new ColorSupport { stdout, stderr } struct threads per-stream support through the reporter builders. maybe_strip_writer is applied only to the StdioConfig.writers returned from LeafExecutionReporter::start (child stdout/stderr and the grouped buffer); banners and summaries use a thin ColorizeExt::style extension that delegates to OwoColorize::if_supports_color.
  • e2e harness gained formatted-snapshot = true (per-step) and switched screen_contents_formatted to vt100::Screen::rows_formatted. Raw \x1b[m resets 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_graph
  • cargo test -p vite_task_bin --test e2e_snapshots
  • cargo clippy --tests on Linux
  • cargo doc --workspace -D warnings
  • CI passes on all platforms (Linux gnu, musl, macOS x86_64/arm64, Windows)

https://claude.ai/code/session_01EAhpw6TzUWExMeF7hdwuF3

claude added 2 commits May 12, 2026 13:03
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
@branchseer branchseer changed the title Simplify FORCE_COLOR handling and strip ANSI escapes at display time feat: improve FORCE_COLOR handling and strip ANSI escapes at display time May 12, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread crates/vite_task_graph/src/config/mod.rs
Comment thread crates/vite_task/src/session/mod.rs
claude added 4 commits May 12, 2026 13:30
`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
@branchseer
Copy link
Copy Markdown
Member Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread crates/vite_task_graph/src/config/mod.rs
Comment thread crates/vite_task/src/session/reporter/summary.rs Outdated
claude added 9 commits May 12, 2026 14:36
`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
@branchseer branchseer changed the title feat: improve FORCE_COLOR handling and strip ANSI escapes at display time feat(cache): store colored task output, strip at display when needed May 12, 2026
@branchseer branchseer changed the title feat(cache): store colored task output, strip at display when needed feat(cache): store colored task logs, strip at display when needed May 12, 2026
@branchseer branchseer merged commit 51e35ea into main May 12, 2026
12 checks passed
@branchseer branchseer deleted the claude/color-env-handling-gzWtn branch May 12, 2026 15:59
branchseer added a commit to voidzero-dev/vite-plus that referenced this pull request May 13, 2026
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>
otnc pushed a commit to otnc/viteplus-ja that referenced this pull request May 13, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cached output should preserve colors

2 participants