fix(console): mop up audit minors — timestamp, filter count, new-line count

- D2: rpcTraceTimestamp used std::localtime (process-wide static tm) guarded by
  a private mutex that can't stop another thread's localtime from clobbering the
  shared buffer. Use localtime_r / localtime_s into a local tm (the codebase
  pattern) and drop the now-useless mutex. Runs on RPC worker threads.
- C1: the toolbar's "<N> matches" label read filter_match_count_ before
  renderOutput recomputed it, so it lagged one frame. Compute the visible/
  filtered set once at the top of render() (before the toolbar) and reuse it in
  renderOutput, so the count and the output share one consistent computation.
- C4: the "N new lines" indicator counted the raw drain count, inflating it with
  lines the active filter hides. Count only newly-added lines that pass the
  current filter, so the badge matches what the user sees on jumping to bottom.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 18:10:27 -05:00
parent e8055888a5
commit 675d434958
2 changed files with 29 additions and 16 deletions

View File

@@ -59,15 +59,16 @@ ConsoleTab* s_rpc_trace_console = nullptr;
std::string rpcTraceTimestamp()
{
// Called on RPC worker threads. std::localtime shares a process-wide static tm, so a private
// mutex here can't stop another thread's localtime call from clobbering it between the call and
// the copy. Use the reentrant variant into a local tm instead (matches the codebase pattern).
std::time_t now = std::time(nullptr);
std::tm localTime{};
static std::mutex timeMutex;
{
std::lock_guard<std::mutex> lock(timeMutex);
if (const std::tm* current = std::localtime(&now)) {
localTime = *current;
}
}
#ifdef _WIN32
localtime_s(&localTime, &now);
#else
localtime_r(&now, &localTime);
#endif
char buffer[16];
std::strftime(buffer, sizeof(buffer), "%H:%M:%S", &localTime);
@@ -297,6 +298,10 @@ void ConsoleTab::render(ConsoleCommandExecutor& exec)
// model_) and the selection/scroll state are touched only on this (main) thread.
drainModel();
// Compute the filtered visible-line set once per frame, BEFORE the toolbar — so its "<N> matches"
// label reflects the current frame (not last frame's stale count). renderOutput reuses the result.
computeVisibleLines(has_text_filter_, filter_lower_);
// Main console layout
ImGui::BeginChild("ConsoleContainer", ImVec2(0, 0), false,
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar);
@@ -756,11 +761,8 @@ void ConsoleTab::renderOutput()
output_scroll_y_ = ImGui::GetScrollY();
scanline_rows_.clear();
// Build the filtered visible-line index list BEFORE mouse handling (screenToTextPos maps
// through visible_indices_). Also yields the text-filter state used for highlighting.
bool has_text_filter = false;
std::string filter_lower;
computeVisibleLines(has_text_filter, filter_lower);
// visible_indices_ / has_text_filter_ / filter_lower_ were already computed once at the top of
// render() (before the toolbar). screenToTextPos maps through visible_indices_, so it's ready.
// Calculate wrapped heights AND build sub-row segments for each visible line. Each
// segment records which bytes of the source text appear on that visual row, so
@@ -785,7 +787,7 @@ void ConsoleTab::renderOutput()
// Draw the visible lines: accent bars, JSON indent guides, selection + filter highlight,
// scanline capture, and the text itself.
drawVisibleLines(padX, line_height, has_text_filter, filter_lower);
drawVisibleLines(padX, line_height, has_text_filter_, filter_lower_);
ImGui::Unindent(padX);
ImGui::PopStyleVar();
@@ -808,7 +810,7 @@ void ConsoleTab::renderOutput()
}
// Filter indicator (text filter only — daemon toggle is already visible in toolbar)
if (has_text_filter) {
if (has_text_filter_) {
char filterBuf[128];
snprintf(filterBuf, sizeof(filterBuf), TR("console_showing_lines"),
static_cast<int>(visible_indices_.size()), model_.size());
@@ -1685,8 +1687,17 @@ void ConsoleTab::drainModel()
// with them so the highlight stays on the text the user selected.
selection_.shiftForEviction(static_cast<int>(dr.popped));
// Track new output that arrived while the user is scrolled up (for the indicator).
scroll_.onLinesAdded(static_cast<int>(dr.added));
// Track new output that arrived while the user is scrolled up (for the "N new lines" indicator).
// Count only the newly-added lines that pass the active filter, so the indicator isn't inflated
// by lines the current filter / errors-only view hides — the user wouldn't see those on jumping
// to the bottom. (The new lines are the last dr.added entries; eviction is from the front.)
ConsoleOutputFilter f{filter_text_, s_daemon_messages_enabled, s_errors_only_enabled,
s_rpc_trace_enabled, s_app_messages_enabled};
const int n = static_cast<int>(model_.size());
int visibleAdded = 0;
for (int i = std::max(0, n - static_cast<int>(dr.added)); i < n; i++)
if (consoleLinePassesFilter(model_[i].text, model_[i].channel, f)) ++visibleAdded;
scroll_.onLinesAdded(visibleAdded);
}
void ConsoleTab::addRpcTraceLine(const std::string& source, const std::string& method)

View File

@@ -175,6 +175,8 @@ private:
std::string context_token_; // hash/address under the cursor at right-click
mutable std::vector<int> visible_indices_; // Cached for selection mapping
bool folding_active_ = true; // fold UI shown only in the unfiltered (foldable) view
bool has_text_filter_ = false; // computed once per frame (before the toolbar draws it)
std::string filter_lower_; // lowercased filter needle for match highlighting
// Wrap layout for the visible lines (segments + per-line heights + cumulative Y),
// recomputed each frame by the pure BuildConsoleLayout (console_text_layout.h) and