From e3b9c989776d6c0153693199ce76d627d827d31a Mon Sep 17 00:00:00 2001 From: Tomas Rimkus Date: Fri, 21 Aug 2026 18:21:30 +0300 Subject: [PATCH] fix(ui): return untruncated errors exactly as received MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- app/src/lib/utils/errorText.ts | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/app/src/lib/utils/errorText.ts b/app/src/lib/utils/errorText.ts index c5ebce73..5b26b38d 100644 --- a/app/src/lib/utils/errorText.ts +++ b/app/src/lib/utils/errorText.ts @@ -28,7 +28,12 @@ const TOAST_ERROR_BUDGET = 400; const MIN_TO_CONDENSE = TOAST_ERROR_BUDGET + 120; 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; /** The original string exactly as received, for copying. Never modified. */ full: string; @@ -40,14 +45,15 @@ export interface CondensedError { export function condenseError(raw: string | null | undefined): CondensedError { // `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 - // copy instead — surrounding blank space should not count toward the budget - // or the omitted count. + // the server sent. The measuring and cutting below works on a trimmed copy + // instead — surrounding blank space should not count toward the budget or + // the omitted count — but `text` is never what gets returned as `display` + // unless the message is actually being shortened. const full = raw ?? ''; const text = full.trim(); 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 @@ -71,9 +77,10 @@ export function condenseError(raw: string | null | undefined): CondensedError { head = head.trimEnd(); 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) { - return { display: text, full, truncated: false, omitted: 0 }; + return { display: full, full, truncated: false, omitted: 0 }; } return { display: `${head} …`, full, truncated: true, omitted };