fix(ui): return untruncated errors exactly as received

Follow-up to discussion_r3831361300 on #1058.

Both non-truncated return paths handed back the trimmed working copy, so
an error like "  request timed out\n" came back altered even though
nothing had been omitted. That contradicted the stated intent that short
errors pass through untouched, and left `display` differing from `full`
for no reason.

`display` is now byte-identical to `full` whenever `truncated` is false —
the trimmed copy is only used for measuring against the budget and for
building the shortened head. Documented on the field.

Verified across padded short errors, clean short errors, empty and
whitespace-only input, and either side of the threshold: display === full
on every untruncated case, and `full` matches the input exactly in all of
them.

One visible consequence: with whitespace-pre-wrap on the description, an
error carrying leading or trailing newlines now renders with that blank
space. Trivial for the messages this sees in practice, and the
alternative was silently editing the text.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Tomas Rimkus
2026-10-04 00:00:15 +00:00
committed by capy-ai-staging[bot]
co-authored by Claude Opus 5
parent 96c6f9cad9
commit e3b9c98977
+14 -7
View File
@@ -28,7 +28,12 @@ const TOAST_ERROR_BUDGET = 400;
const MIN_TO_CONDENSE = TOAST_ERROR_BUDGET + 120; const MIN_TO_CONDENSE = TOAST_ERROR_BUDGET + 120;
export interface CondensedError { export interface CondensedError {
/** What to show in the toast. Trimmed, and shortened when oversized. */ /** What to show in the toast.
*
* Byte-identical to `full` whenever nothing is omitted, so a message that
* fits is never altered. Only an oversized one is rewritten, into a trimmed
* head followed by an ellipsis.
*/
display: string; display: string;
/** The original string exactly as received, for copying. Never modified. */ /** The original string exactly as received, for copying. Never modified. */
full: string; full: string;
@@ -40,14 +45,15 @@ export interface CondensedError {
export function condenseError(raw: string | null | undefined): CondensedError { export function condenseError(raw: string | null | undefined): CondensedError {
// `full` is what the Copy action hands over, so it stays byte-for-byte what // `full` is what the Copy action hands over, so it stays byte-for-byte what
// the server sent. All the measuring and cutting below works on the trimmed // the server sent. The measuring and cutting below works on a trimmed copy
// copy instead — surrounding blank space should not count toward the budget // instead — surrounding blank space should not count toward the budget or
// or the omitted count. // the omitted count — but `text` is never what gets returned as `display`
// unless the message is actually being shortened.
const full = raw ?? ''; const full = raw ?? '';
const text = full.trim(); const text = full.trim();
if (text.length <= MIN_TO_CONDENSE) { if (text.length <= MIN_TO_CONDENSE) {
return { display: text, full, truncated: false, omitted: 0 }; return { display: full, full, truncated: false, omitted: 0 };
} }
// A traceback's first line is nearly always the message; prefer it whenever // A traceback's first line is nearly always the message; prefer it whenever
@@ -71,9 +77,10 @@ export function condenseError(raw: string | null | undefined): CondensedError {
head = head.trimEnd(); head = head.trimEnd();
const omitted = text.length - head.length; const omitted = text.length - head.length;
// Guard against the boundary search having produced nothing shorter. // Guard against the boundary search having produced nothing shorter. Nothing
// is omitted here either, so the message goes back exactly as it arrived.
if (omitted <= 0) { if (omitted <= 0) {
return { display: text, full, truncated: false, omitted: 0 }; return { display: full, full, truncated: false, omitted: 0 };
} }
return { display: `${head} …`, full, truncated: true, omitted }; return { display: `${head} …`, full, truncated: true, omitted };