From d05cebfb96763af105a02a5a187642fbc9698cf6 Mon Sep 17 00:00:00 2001 From: Wout Stiens <71498452+StiensWout@users.noreply.github.com> Date: Sat, 14 Nov 2026 12:22:34 +0100 Subject: [PATCH 1/2] fix(web): notebook error tracebacks no longer show ANSI color codes IPython colors every saved traceback frame with ANSI SGR sequences, and the notebook preview printed them raw into a plain
. Strip SGR from error
traceback frames only; stream text, sources and literal escape spellings stay
as saved, and the frame newline boundary is unchanged.

Co-authored-by: Claude Opus 5.5 
Co-authored-by: GPT 6.1 Sol 
---
 .../src/components/files/FilePreviewPanel.test.ts | 15 ++++++++++-----
 .../components/files/extendedDocumentPreview.ts   |  8 +++++---
 2 files changed, 15 insertions(+), 8 deletions(-)

diff --git a/apps/web/src/components/files/FilePreviewPanel.test.ts b/apps/web/src/components/files/FilePreviewPanel.test.ts
index d9361ba21244a..7b8bd0592ae30 100644
--- a/apps/web/src/components/files/FilePreviewPanel.test.ts
+++ b/apps/web/src/components/files/FilePreviewPanel.test.ts
@@ -296,16 +296,18 @@ Third}`;
         },
       ],
     });
-    // IPython traceback frames can omit trailing newlines or place ANSI resets after them.
+    // IPython traceback frames carry ANSI SGR colors and can omit trailing newlines.
     const failed = openNotebook(
       bytes(
         JSON.stringify({
           nbformat: 4,
           cells: [
+            { cell_type: "markdown", source: "\u001b[1m# Notes\u001b[0m" },
             {
               cell_type: "code",
-              source: "fail()",
+              source: 'print("\u001b[32m")\nfail()',
               outputs: [
+                { output_type: "stream", text: "\u001b[33mwarning\u001b[0m\n" },
                 {
                   output_type: "error",
                   traceback: [
@@ -330,7 +332,7 @@ Third}`;
                     "\u001b[31mTraceback (most recent call last):\n\u001b[0m",
                     "\u001b[32mCell In[3], line 1\n----> 1 fail()\n\u001b[0m\u001b[39m",
                     "\u001b[31mValueError: preview proof\u001b[0m",
-                    "final entry",
+                    "final entry \\x1b[31m kept",
                   ],
                 },
               ],
@@ -340,7 +342,10 @@ Third}`;
       ),
     );
     if (failed.type !== "notebook") throw new Error("Expected notebook");
-    expect(failed.cells[0]!.outputs).toEqual([
+    expect(failed.cells[0]!.source).toBe("\u001b[1m# Notes\u001b[0m");
+    expect(failed.cells[1]!.source).toBe('print("\u001b[32m")\nfail()');
+    expect(failed.cells[1]!.outputs).toEqual([
+      { kind: "text", text: "\u001b[33mwarning\u001b[0m\n" },
       {
         kind: "text",
         text: "-----\nValueError    Traceback (most recent call last)\nCell In[1], line 4\n----> 4 fail()\nCell In[1], line 2, in fail()\n----> 2     raise ValueError()\nValueError: preview proof",
@@ -351,7 +356,7 @@ Third}`;
       },
       {
         kind: "text",
-        text: "\u001b[31mTraceback (most recent call last):\n\u001b[0m\u001b[32mCell In[3], line 1\n----> 1 fail()\n\u001b[0m\u001b[39m\u001b[31mValueError: preview proof\u001b[0m\nfinal entry",
+        text: "Traceback (most recent call last):\nCell In[3], line 1\n----> 1 fail()\nValueError: preview proof\nfinal entry \\x1b[31m kept",
       },
     ]);
     expect(() => openNotebook(bytes('{"nbformat":3,"cells":[]}'))).toThrow("version 4");
diff --git a/apps/web/src/components/files/extendedDocumentPreview.ts b/apps/web/src/components/files/extendedDocumentPreview.ts
index 6b840a00543a6..78bf09541bc07 100644
--- a/apps/web/src/components/files/extendedDocumentPreview.ts
+++ b/apps/web/src/components/files/extendedDocumentPreview.ts
@@ -431,12 +431,14 @@ 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;
 }
-// IPython traceback frames may omit a final newline or follow it with ANSI SGR sequences.
+// IPython traceback frames carry ANSI SGR colors, which a plain 
 cannot render, and may omit a final newline.
 function tracebackLines(value: unknown): unknown {
   if (!Array.isArray(value)) return value;
-  const frames = value.filter((frame): frame is string => typeof frame === "string");
+  const frames = value
+    .filter((frame): frame is string => typeof frame === "string")
+    .map((frame) => frame.replaceAll(/\u001b\[[\d;:]*m/g, ""));
   return frames.map((frame, index) =>
-    index && !/\n(?:\u001b\[[\d;:]*m)*$/.test(frames[index - 1]!) ? `\n${frame}` : frame,
+    index && !frames[index - 1]!.endsWith("\n") ? `\n${frame}` : frame,
   );
 }
 export function openNotebook(bytes: ArrayBuffer): OfficeWorkerResult {

From 3d1120fa4d5dc502dc21c776e337d430d874dfa7 Mon Sep 17 00:00:00 2001
From: Wout Stiens <71498452+StiensWout@users.noreply.github.com>
Date: Sat, 14 Nov 2026 12:28:59 +0100
Subject: [PATCH 2/2] test(web): trim notebook ANSI case to traceback behavior

Co-authored-by: GPT 6.1 Sol 
Co-Authored-By: Claude Opus 5.5 
---
 apps/web/src/components/files/FilePreviewPanel.test.ts   | 8 ++------
 apps/web/src/components/files/extendedDocumentPreview.ts | 2 +-
 2 files changed, 3 insertions(+), 7 deletions(-)

diff --git a/apps/web/src/components/files/FilePreviewPanel.test.ts b/apps/web/src/components/files/FilePreviewPanel.test.ts
index 7b8bd0592ae30..d689a393d8db2 100644
--- a/apps/web/src/components/files/FilePreviewPanel.test.ts
+++ b/apps/web/src/components/files/FilePreviewPanel.test.ts
@@ -302,12 +302,10 @@ Third}`;
         JSON.stringify({
           nbformat: 4,
           cells: [
-            { cell_type: "markdown", source: "\u001b[1m# Notes\u001b[0m" },
             {
               cell_type: "code",
               source: 'print("\u001b[32m")\nfail()',
               outputs: [
-                { output_type: "stream", text: "\u001b[33mwarning\u001b[0m\n" },
                 {
                   output_type: "error",
                   traceback: [
@@ -342,10 +340,8 @@ Third}`;
       ),
     );
     if (failed.type !== "notebook") throw new Error("Expected notebook");
-    expect(failed.cells[0]!.source).toBe("\u001b[1m# Notes\u001b[0m");
-    expect(failed.cells[1]!.source).toBe('print("\u001b[32m")\nfail()');
-    expect(failed.cells[1]!.outputs).toEqual([
-      { kind: "text", text: "\u001b[33mwarning\u001b[0m\n" },
+    expect(failed.cells[0]!.source).toBe('print("\u001b[32m")\nfail()');
+    expect(failed.cells[0]!.outputs).toEqual([
       {
         kind: "text",
         text: "-----\nValueError    Traceback (most recent call last)\nCell In[1], line 4\n----> 4 fail()\nCell In[1], line 2, in fail()\n----> 2     raise ValueError()\nValueError: preview proof",
diff --git a/apps/web/src/components/files/extendedDocumentPreview.ts b/apps/web/src/components/files/extendedDocumentPreview.ts
index 78bf09541bc07..384e3a3e78dac 100644
--- a/apps/web/src/components/files/extendedDocumentPreview.ts
+++ b/apps/web/src/components/files/extendedDocumentPreview.ts
@@ -431,7 +431,7 @@ 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;
 }
-// IPython traceback frames carry ANSI SGR colors, which a plain 
 cannot render, and may omit a final newline.
+// IPython traceback frames carry ANSI SGR colors a 
 cannot render and may omit a final newline.
 function tracebackLines(value: unknown): unknown {
   if (!Array.isArray(value)) return value;
   const frames = value