perf(console): tail-cap large RPC results without a full copy

A large RPC result was fully split into lines and copied before being
handed to the console, even though only the last few lines could survive
the model's line cap.

- The formatter now takes a line cap and keeps only the last N lines,
  located by a backward newline scan, so a huge result is never fully
  split or copied. JSON classification still keys off the first
  character of the whole result.
- It works over a string view and moves line text through, and fold
  spans are computed over the formatted lines directly instead of a
  separate vector of copies.
- getline semantics are preserved: a trailing newline adds no empty
  line and an empty result yields no lines.

Tests: getline parity for empty, trailing-newline and blank-line inputs,
the backward tail cap, and JSON classification and fold spans over a
capped result.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 08:23:30 -05:00
co-authored by Claude Opus 4.8
parent 116a997a3a
commit f7ed1ae115
5 changed files with 104 additions and 31 deletions
+59 -19
View File
@@ -229,32 +229,53 @@ ConsoleRpcCall BuildConsoleRpcCall(const std::string& command)
return call;
}
std::vector<ConsoleResultLine> FormatConsoleRpcResultLines(const std::string& result,
bool isError)
std::vector<ConsoleResultLine> FormatConsoleRpcResultLines(std::string_view result,
bool isError,
std::size_t maxLines)
{
if (isError) {
return {{"Error: " + result, ConsoleResultLineRole::Error}};
std::string text;
text.reserve(7 + result.size());
text.append("Error: ").append(result);
return {{std::move(text), ConsoleResultLineRole::Error}};
}
std::vector<ConsoleResultLine> lines;
std::string normalized = (result == "null") ? "(no result)" : result;
bool isJson = !normalized.empty() && (normalized[0] == '{' || normalized[0] == '[');
std::istringstream stream(normalized);
std::string line;
while (std::getline(stream, line)) {
const std::string_view normalized = (result == "null") ? std::string_view("(no result)") : result;
if (normalized.empty()) return lines; // getline yields no lines for an empty string
const bool isJson = normalized[0] == '{' || normalized[0] == '[';
// getline parity: split on '\n'; one trailing '\n' terminates the last line (no empty extra).
std::size_t end = normalized.size();
if (normalized[end - 1] == '\n') --end;
// Tail cap: walk back until maxLines lines are covered; start just after the boundary newline.
std::size_t begin = 0;
if (maxLines > 0) {
std::size_t seen = 0;
for (std::size_t i = end; i-- > 0; ) {
if (normalized[i] == '\n' && ++seen == maxLines) { begin = i + 1; break; }
}
}
std::size_t pos = begin;
while (true) {
std::size_t nl = normalized.find('\n', pos);
if (nl == std::string_view::npos || nl > end) nl = end;
const std::string_view line = normalized.substr(pos, nl - pos);
ConsoleResultLineRole role = ConsoleResultLineRole::Result;
if (isJson && !line.empty()) {
std::string trimmed = line;
std::size_t first = trimmed.find_first_not_of(" \t");
if (first != std::string::npos) trimmed = trimmed.substr(first);
if (!trimmed.empty()) {
std::size_t first = line.find_first_not_of(" \t");
if (first != std::string_view::npos) {
const std::string_view trimmed = line.substr(first);
unsigned char firstChar = static_cast<unsigned char>(trimmed[0]);
if (trimmed[0] == '{' || trimmed[0] == '}' ||
trimmed[0] == '[' || trimmed[0] == ']') {
role = ConsoleResultLineRole::JsonBrace;
} else if (trimmed[0] == '"') {
if (trimmed.find("\": ") != std::string::npos ||
trimmed.find("\":") != std::string::npos) {
if (trimmed.find("\":") != std::string_view::npos) {
role = ConsoleResultLineRole::JsonKey;
} else {
role = ConsoleResultLineRole::JsonString;
@@ -268,12 +289,17 @@ std::vector<ConsoleResultLine> FormatConsoleRpcResultLines(const std::string& re
}
}
}
lines.push_back({line, role});
lines.push_back({std::string(line), role});
if (nl >= end) break;
pos = nl + 1;
}
return lines;
}
std::vector<int> ComputeConsoleFoldSpans(const std::vector<std::string>& lines)
namespace {
template <typename Lines, typename TextOf>
std::vector<int> computeFoldSpans(const Lines& lines, TextOf textOf)
{
std::vector<int> spans(lines.size(), 0);
@@ -294,14 +320,16 @@ std::vector<int> ComputeConsoleFoldSpans(const std::vector<std::string>& lines)
};
for (std::size_t i = 0; i < lines.size(); ++i) {
const char last = lastNonSpace(lines[i]);
const std::string& li = textOf(lines[i]);
const char last = lastNonSpace(li);
if (last != '{' && last != '[') continue; // not a block opener
const std::size_t indent = leadingSpaces(lines[i]);
const std::size_t indent = leadingSpaces(li);
const char wantClose = (last == '{') ? '}' : ']';
// Matching closer = the next line at the same indentation whose first non-space char
// is the corresponding closing bracket (pretty-printed JSON has aligned brackets).
for (std::size_t j = i + 1; j < lines.size(); ++j) {
if (leadingSpaces(lines[j]) == indent && firstNonSpace(lines[j]) == wantClose) {
const std::string& lj = textOf(lines[j]);
if (leadingSpaces(lj) == indent && firstNonSpace(lj) == wantClose) {
if (j - i >= 2) spans[i] = static_cast<int>(j - i); // >=1 inner line
break;
}
@@ -310,5 +338,17 @@ std::vector<int> ComputeConsoleFoldSpans(const std::vector<std::string>& lines)
return spans;
}
} // namespace
std::vector<int> ComputeConsoleFoldSpans(const std::vector<std::string>& lines)
{
return computeFoldSpans(lines, [](const std::string& s) -> const std::string& { return s; });
}
std::vector<int> ComputeConsoleResultFoldSpans(const std::vector<ConsoleResultLine>& lines)
{
return computeFoldSpans(lines, [](const ConsoleResultLine& l) -> const std::string& { return l.text; });
}
} // namespace ui
} // namespace dragonx