fix(console): keep JSON folding active under channel filters
JSON folding in the console was bypassed in any filtered view, including when only a channel toggle (daemon, RPC or app) was active. A collapsed block silently expanded the moment a channel was toggled. - Folding is now kept active unless the view is text-filtered or errors-only, which are flat so that every match stays reachable. Channel toggles keep folding, since JSON blocks are atomic per channel. - The fold triangles and the collapsed " ... }" summary are drawn only in the fold-aware view, so the glyph and the rendered block can never disagree. User-visible: JSON results stay foldable while channel filters are applied. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -915,6 +915,26 @@ void ConsoleTab::renderOutput()
|
||||
|
||||
// ── renderOutput sub-steps ───────────────────────────────────────────────────
|
||||
|
||||
namespace {
|
||||
// Folding applies unless the view is text-filtered or errors-only (flat views).
|
||||
inline bool consoleFoldingActive(bool hasTextFilter, bool errorsOnly) { return !(hasTextFilter || errorsOnly); }
|
||||
|
||||
// Visible-index build for the fold-aware view. A collapsed opener that passes the filter hides its
|
||||
// interior + closer (foldSpan lines); a hidden opener hides only itself (blocks are atomic per channel).
|
||||
template <class SpanFn, class CollapsedFn, class PassFn>
|
||||
void BuildConsoleVisibleIndices(std::vector<int>& out, int n, SpanFn span, CollapsedFn collapsed, PassFn pass)
|
||||
{
|
||||
out.clear();
|
||||
for (int i = 0; i < n; ) {
|
||||
if (!pass(i)) { ++i; continue; }
|
||||
out.push_back(i);
|
||||
const int fs = span(i);
|
||||
if (fs > 0 && collapsed(i)) i += fs + 1;
|
||||
else ++i;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void ConsoleTab::computeVisibleLines(bool& hasTextFilter, std::string& filterLower)
|
||||
{
|
||||
// Memoize: rebuild the visible set only when the model changed or the filter state changed.
|
||||
@@ -944,10 +964,12 @@ void ConsoleTab::computeVisibleLines(bool& hasTextFilter, std::string& filterLow
|
||||
bool has_filter = hasTextFilter || !outputFilter.daemonMessagesEnabled ||
|
||||
!outputFilter.rpcTraceEnabled || !outputFilter.appMessagesEnabled ||
|
||||
outputFilter.errorsOnly;
|
||||
// Folding is only honored in the unfiltered view; the filtered view is flat. Record it so the
|
||||
// fold triangles aren't drawn/clickable while filtering (else clicks would silently mutate the
|
||||
// Folding is honored unless the view is text-filtered or errors-only (those are flat so every match is
|
||||
// reachable). Channel toggles alone keep folding: JSON blocks are atomic per channel. Record it so the
|
||||
// fold triangles aren't drawn/clickable in the flat view (else clicks would silently mutate the
|
||||
// collapsed flag with no visible effect and the glyph would disagree with the rendered block).
|
||||
folding_active_ = !has_filter;
|
||||
const bool folding = consoleFoldingActive(hasTextFilter, outputFilter.errorsOnly);
|
||||
folding_active_ = folding;
|
||||
const int n = static_cast<int>(model_.size());
|
||||
const std::uint64_t seqFirst = model_.firstSeq();
|
||||
const int flatFlags = (outputFilter.daemonMessagesEnabled ? 1 : 0) | (outputFilter.errorsOnly ? 2 : 0) |
|
||||
@@ -955,31 +977,29 @@ void ConsoleTab::computeVisibleLines(bool& hasTextFilter, std::string& filterLow
|
||||
// Filtered (flat) view with the exact same filter as the last pass and a model that only appended/evicted
|
||||
// since: drop evicted indices, rebase, test only the new lines. Identical to the full scan below (line text +
|
||||
// channel are immutable; the flat view ignores folds) without its per-drain O(total chars) lowercase pass.
|
||||
const bool extended = has_filter && vis_flat_valid_ && vis_flat_flags_ == flatFlags &&
|
||||
const bool extended = !folding && vis_flat_valid_ && vis_flat_flags_ == flatFlags &&
|
||||
vis_flat_filter_ == outputFilter.text &&
|
||||
ExtendFlatVisibleIndices(visible_indices_, vis_flat_first_, vis_flat_end_, seqFirst, n,
|
||||
[&](int i) { return consoleLinePassesFilter(model_[i].text, model_[i].channel, outputFilter); });
|
||||
if (extended) {
|
||||
// visible_indices_ updated in place
|
||||
} else if (has_filter) {
|
||||
} else if (!folding) {
|
||||
visible_indices_.clear();
|
||||
// Filtered view is flat — folding is bypassed so every match is reachable.
|
||||
// Flat view — folding is bypassed so every match is reachable.
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (!consoleLinePassesFilter(model_[i].text, model_[i].channel, outputFilter)) continue;
|
||||
visible_indices_.push_back(i);
|
||||
}
|
||||
} else {
|
||||
visible_indices_.clear();
|
||||
// No filter — show all lines, but hide the interior of any collapsed JSON block
|
||||
// (its opener stays visible; the opener + foldSpan lines through the closer are skipped).
|
||||
for (int i = 0; i < n; ) {
|
||||
visible_indices_.push_back(i);
|
||||
const ConsoleModelLine& line = model_[i];
|
||||
if (line.foldSpan > 0 && line.collapsed) i += line.foldSpan + 1; // skip inner + closer
|
||||
else i++;
|
||||
}
|
||||
// Fold-aware view: hide the interior of any collapsed JSON block (its opener stays visible; the
|
||||
// opener + foldSpan lines through the closer are skipped). Channel toggles still apply.
|
||||
BuildConsoleVisibleIndices(visible_indices_, n,
|
||||
[&](int i) { return model_[i].foldSpan; },
|
||||
[&](int i) { return model_[i].collapsed; },
|
||||
[&](int i) { return !has_filter ||
|
||||
consoleLinePassesFilter(model_[i].text, model_[i].channel, outputFilter); });
|
||||
}
|
||||
vis_flat_valid_ = has_filter;
|
||||
vis_flat_valid_ = !folding;
|
||||
vis_flat_flags_ = flatFlags;
|
||||
vis_flat_filter_ = outputFilter.text;
|
||||
vis_flat_first_ = seqFirst;
|
||||
@@ -1125,7 +1145,7 @@ void ConsoleTab::drawVisibleLines(float padX, float lineHeight, bool hasTextFilt
|
||||
|
||||
// JSON fold toggle — a small triangle in the left gutter for block-opener lines.
|
||||
// Openers carry no accent bar (result/JSON channels), so the gutter is free. Only shown in the
|
||||
// unfiltered view, where folding actually applies (see folding_active_ in computeVisibleLines).
|
||||
// fold-aware view, where folding actually applies (see folding_active_ in computeVisibleLines).
|
||||
if (folding_active_ && line.foldSpan > 0) {
|
||||
// Size from the (DPI/density-scaled) gutter width, not the zoomed font, and center it in
|
||||
// the gutter band [origin-padX, origin) so the glyph and its clickable cell stay aligned.
|
||||
@@ -1231,7 +1251,7 @@ void ConsoleTab::drawVisibleLines(float padX, float lineHeight, bool hasTextFilt
|
||||
// Collapsed JSON block: append a dim " ... }" summary after the opener's text so the
|
||||
// fold reads as e.g. "outputs": [ ... ]. Anchor it on the opener's LAST visual row
|
||||
// and that row's measured width so it stays put even if the opener wraps.
|
||||
if (line.foldSpan > 0 && line.collapsed && !segs.empty()) {
|
||||
if (folding_active_ && line.foldSpan > 0 && line.collapsed && !segs.empty()) {
|
||||
char closeCh = '}';
|
||||
for (size_t k = line.text.size(); k-- > 0; ) {
|
||||
if (line.text[k] != ' ' && line.text[k] != '\t') {
|
||||
|
||||
Reference in New Issue
Block a user