fix(console): stop the Clear button crashing the tab

render() computes visible_indices_ (indices into model_) once per frame at the
top, before the toolbar. The toolbar's Clear button called clear() → model_.clear(),
emptying model_ mid-frame (the "cleared" marker is only queued, drained next
frame). renderOutput() then indexed model_[visible_indices_[vi]] with the stale
indices → out-of-bounds → crash.

clear() now also drops visible_indices_ and the selection (both hold line indices
into model_), so this frame's renderOutput iterates zero lines and computeVisibleLines
rebuilds them next frame. The right-click "Clear console" menu item now routes
through clear() instead of a bare model_.clear() so it's covered too.

Adversarially verified: no other same-frame path indexes the emptied model
(fold-toggle, ingest, and selectAll are bounds-guarded).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-15 23:09:04 -05:00
parent 7f414b3dcf
commit faf77de9ce

View File

@@ -511,8 +511,7 @@ void ConsoleTab::renderToolbar(ConsoleCommandExecutor& exec)
// Clear button
if (TactileButton(TR("console_clear"), ImVec2(0, 0), schema::UI().resolveFont("button"))) {
clear();
selection_.clear();
clear(); // also drops the stale visible_indices_ + selection (see ConsoleTab::clear)
}
ImGui::SameLine();
@@ -1183,9 +1182,9 @@ void ConsoleTab::drawOutputContextMenu()
}
ImGui::Separator();
if (ImGui::MenuItem(TR("console_clear_console"))) {
// View-only clear (main thread) — drop the visible lines and the selection.
model_.clear();
selection_.clear();
// View-only clear — route through clear() so the stale visible_indices_/selection are dropped too
// (a bare model_.clear() mid-frame would leave renderOutput() indexing the emptied model_ → crash).
clear();
}
ImGui::EndPopup();
}
@@ -2004,6 +2003,12 @@ void ConsoleTab::clear()
// View-only clear (main thread). The executor keeps its own log cursors, so new output
// still appends. The "cleared" line is ingested and appears on the next frame's drain.
model_.clear();
// visible_indices_ was computed at the top of render() (line 311), BEFORE the toolbar's Clear button
// ran; those indices now point past the emptied model_. renderOutput() (this same frame, after the
// toolbar) indexes model_[visible_indices_[vi]] — so drop them (and the selection, which also holds
// line indices) here to avoid an out-of-bounds crash. computeVisibleLines() rebuilds them next frame.
visible_indices_.clear();
selection_.clear();
stop_confirm_pending_ = false; // a pending 'stop' confirmation is cancelled by clearing
addLine(TR("console_cleared"), ConsoleChannel::Info);
}