From aa627589c5e5dce58c1b709cda8964ffae2bdd40 Mon Sep 17 00:00:00 2001 From: Geezy Date: Fri, 6 Nov 2026 11:07:38 +0100 Subject: [PATCH 1/2] fix(web): notebook previews show error tracebacks line by line without color codes Jupyter saves an error output's traceback as a list of lines colored with ANSI escapes. The preview joined the entries with no separator and kept the escapes, so tracebacks rendered as run-together lines full of "[0;31m". Join the entries with newlines and strip the color codes, as Jupyter does. Co-Authored-By: Claude Opus 5.5 --- .../components/files/FilePreviewPanel.test.ts | 41 +++++++++++++++++++ .../files/extendedDocumentPreview.ts | 11 ++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/files/FilePreviewPanel.test.ts b/apps/web/src/components/files/FilePreviewPanel.test.ts index 18f725c3ba68e..8e58d4d9b8999 100644 --- a/apps/web/src/components/files/FilePreviewPanel.test.ts +++ b/apps/web/src/components/files/FilePreviewPanel.test.ts @@ -286,6 +286,47 @@ describe("document preview contents", () => { ).toThrow("500 cells"); }); + it("shows notebook error tracebacks as plain lines", () => { + // Traceback saved by IPython for `1 / 0`. + const result = openNotebook( + bytes( + JSON.stringify({ + nbformat: 4, + cells: [ + { + cell_type: "code", + source: "1 / 0", + outputs: [ + { + output_type: "error", + ename: "ZeroDivisionError", + evalue: "division by zero", + traceback: [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mZeroDivisionError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[1], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;241;43m1\u001b[39;49m\u001b[38;5;241;43m/\u001b[39;49m\u001b[38;5;241;43m0\u001b[39m\n", + "\u001b[0;31mZeroDivisionError\u001b[0m: division by zero", + ], + }, + ], + }, + ], + }), + ), + ); + expect(result.type === "notebook" && result.cells[0]?.outputs).toEqual([ + { + kind: "text", + text: [ + "---------------------------------------------------------------------------", + "ZeroDivisionError Traceback (most recent call last)", + "Cell In[1], line 1\n----> 1 1/0\n", + "ZeroDivisionError: division by zero", + ].join("\n"), + }, + ]); + }); + it("bounds repeated OpenDocument spreadsheet cells and keeps sheet names and values", async () => { const archive = new JSZip().file( "content.xml", diff --git a/apps/web/src/components/files/extendedDocumentPreview.ts b/apps/web/src/components/files/extendedDocumentPreview.ts index 46cf7dbb67a83..faa209755909f 100644 --- a/apps/web/src/components/files/extendedDocumentPreview.ts +++ b/apps/web/src/components/files/extendedDocumentPreview.ts @@ -3,6 +3,7 @@ import { IMAGE_DIMENSIONS_HEADER_BYTES, readImageDimensions, } from "@t3tools/shared/imageDimensions"; +import { terminalOutputPlainText } from "@t3tools/shared/terminalOutput"; import { XMLParser, XMLValidator } from "fast-xml-parser"; import { decodeHTMLAttribute, decodeHTMLStrict as decodeEntities } from "entities"; import type JSZip from "jszip"; @@ -423,6 +424,12 @@ function notebookText(value: unknown, limit = 256_000): string { if (result.length > limit) throw new Error("A notebook cell is too large to preview."); return result; } +// Jupyter shows each traceback entry as its own line and colors it with ANSI escapes. +function notebookTraceback(value: unknown): unknown { + return Array.isArray(value) + ? terminalOutputPlainText(value.filter((line) => typeof line === "string").join("\n")) + : value; +} export function openNotebook(bytes: ArrayBuffer): OfficeWorkerResult { if (bytes.byteLength > 8 * 1024 * 1024) throw new Error("Notebook preview limited to files under 8 MB."); @@ -454,7 +461,9 @@ export function openNotebook(bytes: ArrayBuffer): OfficeWorkerResult { if (html.trim() && htmlImagesFit(html)) return { kind: "html" as const, text: html }; return { kind: "text" as const, - text: notebookText(output.text ?? data["text/plain"] ?? output.traceback), + text: notebookText( + output.text ?? data["text/plain"] ?? notebookTraceback(output.traceback), + ), }; }) .filter((output) => output.text.trim().length > 0), From 2bceffa3cf687cb03491de48eccdbe3608dab0e4 Mon Sep 17 00:00:00 2001 From: Geezy Date: Sat, 14 Nov 2026 11:37:57 +0100 Subject: [PATCH 2/2] fix(shared): strip colon-form SGR from plain text Cover true-color colon parameters in the existing notebook traceback case. Implemented and verified with gpt-6.1-sol in the Codex harness through T3 Code. --- apps/web/src/components/files/FilePreviewPanel.test.ts | 2 +- packages/shared/src/terminalOutput.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/files/FilePreviewPanel.test.ts b/apps/web/src/components/files/FilePreviewPanel.test.ts index 6b58ee4a6e604..c4c3905ba9853 100644 --- a/apps/web/src/components/files/FilePreviewPanel.test.ts +++ b/apps/web/src/components/files/FilePreviewPanel.test.ts @@ -328,7 +328,7 @@ Third}`; output_type: "error", traceback: [ "\u001b[31mTraceback (most recent call last):\n\u001b[0m", - "\u001b[32mCell In[3], line 1\n----> 1 fail()\n\u001b[0m\u001b[39m", + "\u001b[38:2::255:0:0mCell In[3], line 1\n----> 1 fail()\n\u001b[0m\u001b[39m", "\u001b[31mValueError: preview proof\u001b[0m", "final entry", ], diff --git a/packages/shared/src/terminalOutput.ts b/packages/shared/src/terminalOutput.ts index bae1a76041ed7..523092db1b3d7 100644 --- a/packages/shared/src/terminalOutput.ts +++ b/packages/shared/src/terminalOutput.ts @@ -890,7 +890,7 @@ export function terminalOutputSpans( return spans; } -const SGR_PATTERN = new RegExp(`${ESC}\\[[0-9;]*m`, "gu"); +const SGR_PATTERN = new RegExp(`${ESC}\\[[0-9;:]*m`, "gu"); export function terminalOutputPlainText(text: string): string { return text.replace(SGR_PATTERN, "");