Skip to content

fix(cli, printer): Close the chrome channel under --quiet - #1088

Open
JeanMertz wants to merge 9 commits into
mainfrom
fix/quiet-closes-the-chrome-channel
Open

fix(cli, printer): Close the chrome channel under --quiet#1088
JeanMertz wants to merge 9 commits into
mainfrom
fix/quiet-closes-the-chrome-channel

Conversation

@JeanMertz

Copy link
Copy Markdown
Collaborator

-q promised to suppress all output and delivered none of it: the only
thing consulting the flag was the tracing setup, so a quiet run still
printed the assistant's response, tool call headers, retry notices, and
status lines.

-q now silences chrome — the run's commentary on itself, everything on
stderr. Command output and the assistant's response keep stdout, because
that is the data the caller asked for, and a failing run still reports
its error. >/dev/null and 2>/dev/null remain the way to drop those,
which is why the flag does not try to: the shell already expresses
"silence everything", and cannot express "chrome but not errors".

jp -q query "summarize this" > answer.md    # no headers, no spinner

The policy lives on the Printer, the one place every emission passes,
rather than at each of the call sites that emit. Chrome::Silenced
discards eprint, eprintln, erase_line, and anything written
through err_writer, and leaves stdout and the prompt channel alone.
Chrome that repaints a line in place asks chrome_repaints, which
answers for a silenced channel and a JSON consumer at once, so the
waiting timer does not spawn a task to format a line nobody receives.

Scripts reading JP's stderr under -q saw output before and see none
now. jp c grep -q is unchanged: reporting a match through the exit
status alone is grep's own convention, and it keeps it.

Base automatically changed from json-no-escapes to main September 4, 2026 13:15
The flag is documented as suppressing all output including errors, but
the only thing it does is drop the stderr tracing layer. Every printer
emission — the assistant response, tool headers, retry notices — ignores
it, and the run's error message is written with `eprintln!` regardless.

The ticket records where the fix belongs (the printer, the one place
that sees every emission) and what has to be decided first: whether `-q`
covers chrome only, everything, or graduated levels. It also collects
the three documents that disagree about what the flag means.

Signed-off-by: Jean Mertz <git@jeanmertz.com>
Waiting on a conversation another run holds ends in a prompt offering to
keep waiting, start a new conversation, fork, or cancel. That prompt was
rendered on the error stream, which is where the run's chrome goes
rather than where it talks to the user.

Nothing changes for a normal run, where both land on the same terminal.
It matters for a run that redirects stderr, and for one that silences
its chrome: a prompt that never appears while the run blocks waiting for
an answer to it is a hang with no explanation.

Prompts have their own channel, `/dev/tty` where one is available, which
survives both.

Signed-off-by: Jean Mertz <git@jeanmertz.com>
`-q` promised to suppress all output and delivered none of it: the only
thing consulting the flag was the tracing setup, so a quiet run still
printed the assistant's response, tool call headers, retry notices, and
status lines.

`-q` now silences chrome — the run's commentary on itself, everything on
stderr. Command output and the assistant's response keep stdout, because
that is the data the caller asked for, and a failing run still reports
its error. `>/dev/null` and `2>/dev/null` remain the way to drop those,
which is why the flag does not try to: the shell already expresses
"silence everything", and cannot express "chrome but not errors".

    jp -q query "summarize this" > answer.md    # no headers, no spinner

The policy lives on the `Printer`, the one place every emission passes,
rather than at each of the call sites that emit. `Chrome::Silenced`
discards `eprint`, `eprintln`, `erase_line`, and anything written
through `err_writer`, and leaves stdout and the prompt channel alone.
Chrome that repaints a line in place asks `chrome_repaints`, which
answers for a silenced channel and a JSON consumer at once, so the
waiting timer does not spawn a task to format a line nobody receives.

Scripts reading JP's stderr under `-q` saw output before and see none
now. `jp c grep -q` is unchanged: reporting a match through the exit
status alone is grep's own convention, and it keeps it.

Signed-off-by: Jean Mertz <git@jeanmertz.com>
The flag table said `-q` does not affect tracing, which left open
whether the log file and the `JP_DEBUG` path report survive it. They do:
both are developer channels, asked for by name, and a user asking for a
quiet terminal is not asking to lose the record of the run.

Signed-off-by: Jean Mertz <git@jeanmertz.com>
Signed-off-by: Jean Mertz <git@jeanmertz.com>
@JeanMertz
JeanMertz force-pushed the fix/quiet-closes-the-chrome-channel branch from e4a3297 to 0a06573 Compare September 5, 2026 16:45
`jp -q plugin update` still announced "Refreshing plugin registry..." and
its result, because those lines went to `std::io::stderr()` directly
rather than through the printer that `--quiet` closes. The install path
and the auto-install path that runs when an unknown subcommand matches a
registry plugin printed their progress the same way.

All of it is chrome and now goes through the printer, so a quiet run is
quiet and a `--format json` run gets NDJSON records instead of raw text
among them.

The approval questions those paths ask are not chrome and keep their own
channel: `Install and run it?`, `Run it?`, and the third-party
confirmation move to the prompt writer, which reaches the terminal even
when stderr is redirected or silenced. A question the run blocks on has
to be visible.

`resolve_plugin_binary` and the two helpers behind it take the printer as
an argument, since they run before any renderer exists. Deciding which
plugins are outdated is now separate from reporting it, which is what
the new tests exercise: the network is not reachable from a unit test,
but the reporting is.

Signed-off-by: Jean Mertz <git@jeanmertz.com>
`jp query "..." > answer.txt` captured the role header announcing who
speaks — `── jp (anthropic/claude-opus-5) ──` — along with the answer,
and so did the echo of the user's own request. [RFD 048] puts the
assistant's response on stdout and everything the run says about itself
on stderr; the framing was on the wrong side of that line, and no amount
of `--quiet` could move it, because `--quiet` closes the chrome channel
and the framing was not on it.

A redirected or piped run gets the answer alone now. In a terminal
nothing changes: both streams are serialized through the same printer
worker, so the header still lands immediately before the content it
introduces.

`jp conversation print` is unaffected and keeps its headers on stdout.
There the same line is not narration but document structure, delimiting
one stored turn from the next, and a transcript redirected to a file
needs it. Both flows share `TurnView`, which is what let one classify
the header for both; it now carries a `RenderFlow` naming which of the
two it renders for, and the framing follows from that.

[RFD 048]: https://jp.computer/rfd/048

Signed-off-by: Jean Mertz <git@jeanmertz.com>
`JP_DEBUG=1 jp query "..." > answer.txt` said nothing about the trace
log it had just written. The report was gated on stdout being a
terminal, so redirecting or piping the answer withdrew it — from the one
person who had asked for it by name.

`JP_DEBUG` is the whole question now. The report is developer output
requested explicitly, and it goes to stderr, so where the run's own
streams point has no bearing on it. A failed run still reports
unconditionally.

The gate was there to protect a program that owns the screen from two
uninvited lines: in `jp … | fzf`, stderr is still the terminal, and the
report lands in fzf's layout. That case is now the caller's to avoid, by
unsetting the variable or redirecting stderr, which is the trade for
having the report show up at all in the far more common one.

Signed-off-by: Jean Mertz <git@jeanmertz.com>
`jp query "..." > answer.txt` came back with two blank lines wherever
the assistant had called a tool mid-answer, one of them empty of any
reason to exist.

A tool call is framed by two gaps: the content above it ends with its
own trailing blank line, and a second blank line follows the chrome to
separate it from the content below. Both were on stdout while the chrome
between them was on stderr, so a reader capturing the answer alone got
the two gaps and nothing in between.

The second gap goes out on the chrome channel, next to the chrome it
spaces from. Stdout keeps one blank line between the two blocks, which
is what two content blocks want anyway, and a terminal renders exactly
as before: both streams are serialized through the same printer worker,
so the order across them holds.

The gap deferred when a reasoning region continues across the tool call
is unchanged and stays on stdout. It ends up in front of reasoning
rather than after chrome, with reasoning above it, so both sides of it
are content.

Signed-off-by: Jean Mertz <git@jeanmertz.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.

1 participant