improvement: better tracebacks, padding, remove button for static files (#6912)

This commit is contained in:
Myles Scolnick
2025-10-23 20:42:12 -04:00
committed by GitHub
parent c21770b908
commit a3c491dc37
12 changed files with 206 additions and 67 deletions

3
.gitignore vendored
View File

@@ -74,9 +74,6 @@ docs/_build/
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.13

View File

@@ -30,6 +30,7 @@ import type { CellId } from "@/core/cells/ids";
import { insertDebuggerAtLine } from "@/core/codemirror/editing/debugging";
import { aiEnabledAtom } from "@/core/config/config";
import { getRequestClient } from "@/core/network/requests";
import { isStaticNotebook } from "@/core/static/static-state";
import { isWasm } from "@/core/wasm/utils";
import { renderHTML } from "@/plugins/core/RenderHTML";
import { copyToClipboard } from "@/utils/copy";
@@ -70,6 +71,17 @@ export const MarimoTracebackOutput = ({
// Get last traceback info
const tracebackInfo = extractAllTracebackInfo(traceback)?.at(0);
// Don't show in wasm or static notebooks
const showDebugger =
tracebackInfo &&
tracebackInfo.kind === "cell" &&
!isWasm() &&
!isStaticNotebook();
const showAIFix = onRefactorWithAI && aiEnabled && !isStaticNotebook();
const showSearch = !isStaticNotebook();
const handleRefactorWithAI = (triggerImmediately: boolean) => {
onRefactorWithAI?.({
prompt: `My code gives the following error:\n\n${lastTracebackLine}`,
@@ -104,14 +116,14 @@ export const MarimoTracebackOutput = ({
</AccordionItem>
</Accordion>
<div className="flex gap-2">
{onRefactorWithAI && aiEnabled && (
{showAIFix && (
<AIFixButton
tooltip="Fix with AI"
openPrompt={() => handleRefactorWithAI(false)}
applyAutofix={() => handleRefactorWithAI(true)}
/>
)}
{tracebackInfo && tracebackInfo.kind === "cell" && !isWasm() && (
{showDebugger && (
<Tooltip content={"Attach pdb to the exception point."}>
<Button
size="xs"
@@ -125,50 +137,52 @@ export const MarimoTracebackOutput = ({
</Button>
</Tooltip>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild={true}>
<Button size="xs" variant="text">
Get help
<ChevronDown className="h-3 w-3 ml-1" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuItem asChild={true}>
<a
target="_blank"
href={`https://www.google.com/search?q=${encodeURIComponent(lastTracebackLine)}`}
rel="noreferrer"
{showSearch && (
<DropdownMenu>
<DropdownMenuTrigger asChild={true}>
<Button size="xs" variant="text">
Get help
<ChevronDown className="h-3 w-3 ml-1" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuItem asChild={true}>
<a
target="_blank"
href={`https://www.google.com/search?q=${encodeURIComponent(lastTracebackLine)}`}
rel="noreferrer"
>
<SearchIcon className="h-4 w-4 mr-2" />
Search on Google
<ExternalLinkIcon className="h-3 w-3 ml-auto" />
</a>
</DropdownMenuItem>
<DropdownMenuItem asChild={true}>
<a
target="_blank"
href="https://marimo.io/discord?ref=notebook"
rel="noopener"
>
<MessageCircleIcon className="h-4 w-4 mr-2" />
Ask in Discord
<ExternalLinkIcon className="h-3 w-3 ml-auto" />
</a>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
// Strip HTML from the traceback
const div = document.createElement("div");
div.innerHTML = traceback;
const textContent = div.textContent || "";
copyToClipboard(textContent);
}}
>
<SearchIcon className="h-4 w-4 mr-2" />
Search on Google
<ExternalLinkIcon className="h-3 w-3 ml-auto" />
</a>
</DropdownMenuItem>
<DropdownMenuItem asChild={true}>
<a
target="_blank"
href="https://marimo.io/discord?ref=notebook"
rel="noopener"
>
<MessageCircleIcon className="h-4 w-4 mr-2" />
Ask in Discord
<ExternalLinkIcon className="h-3 w-3 ml-auto" />
</a>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
// Strip HTML from the traceback
const div = document.createElement("div");
div.innerHTML = traceback;
const textContent = div.textContent || "";
copyToClipboard(textContent);
}}
>
<CopyIcon className="h-4 w-4 mr-2" />
Copy to clipboard
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<CopyIcon className="h-4 w-4 mr-2" />
Copy to clipboard
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</div>
);

View File

@@ -21,13 +21,16 @@ export const VerticalLayoutWrapper: React.FC<PropsWithChildren<Props>> = ({
<div
className={cn(
"px-1 sm:px-16 md:px-20 xl:px-24 print:px-0 print:pb-0",
// Large mobile bottom padding due to mobile browser navigation bar
"pb-24 sm:pb-12",
className,
)}
>
<div
className={cn(
// Large mobile bottom padding due to mobile browser navigation bar
"m-auto pb-24 sm:pb-12",
"m-auto",
// This padding needs to be the same from above to be correctly applied
"pb-24 sm:pb-12",
appConfig.width === "compact" &&
"max-w-(--content-width) min-w-[400px]",
appConfig.width === "medium" &&

View File

@@ -210,8 +210,18 @@ describe("notebookStateFromSession", () => {
it("handles console outputs in session cell", () => {
const consoleOutputs = [
{ type: "stream", name: "stdout", text: "Hello stdout" } as const,
{ type: "stream", name: "stderr", text: "Hello stderr" } as const,
{
type: "stream",
name: "stdout",
text: "Hello stdout",
mimetype: "text/plain",
} as const,
{
type: "stream",
name: "stderr",
text: "Hello stderr",
mimetype: "text/plain",
} as const,
];
const session = createSession([
createSessionCell("cell-1", [], consoleOutputs),
@@ -356,7 +366,14 @@ describe("notebookStateFromSession", () => {
createSessionCell(
"cell-1",
[],
[{ type: "stream", name: "stdout", text: "output" }],
[
{
type: "stream",
name: "stdout",
text: "output",
mimetype: "text/plain",
},
],
),
]);
const notebook = createNotebook([

View File

@@ -229,7 +229,7 @@ function createCellRuntimeFromSession(
return {
channel: consoleOutput.name === "stderr" ? "stderr" : "stdout",
data: consoleOutput.text,
mimetype: "text/plain",
mimetype: consoleOutput.mimetype ?? "text/plain",
timestamp: DEFAULT_TIMESTAMP,
};
}),

View File

@@ -126,6 +126,31 @@ components:
type: object
StreamOutput:
properties:
mimetype:
enum:
- application/json
- application/vnd.marimo+error
- application/vnd.marimo+traceback
- application/vnd.marimo+mimebundle
- application/vnd.vega.v5+json
- application/vnd.vegalite.v5+json
- application/vnd.jupyter.widget-view+json
- image/png
- image/svg+xml
- image/tiff
- image/avif
- image/bmp
- image/gif
- image/jpeg
- video/mp4
- video/mpeg
- text/html
- text/plain
- text/markdown
- text/latex
- text/csv
nullable: true
type: string
name:
enum:
- stdout
@@ -141,6 +166,7 @@ components:
- type
- name
- text
- mimetype
type: object
TimeMetadata:
properties:

View File

@@ -30,6 +30,7 @@ class StreamOutput(BaseDict):
type: Literal["stream"]
name: Literal["stdout", "stderr"]
text: str
mimetype: Optional[KnownMimeType]
class StreamMediaOutput(BaseDict):

View File

@@ -151,6 +151,7 @@ def serialize_session_view(
if console_out.channel == CellChannel.STDERR
else "stdout",
text=str(console_out.data),
mimetype=console_out.mimetype,
)
)
@@ -235,22 +236,33 @@ def deserialize_session(session: NotebookSessionV1) -> SessionView:
else:
is_stderr = console["name"] == "stderr"
data = console["text"]
# HACK: We need to detect tracebacks in stderr by checking for HTML
# formatting.
is_traceback = (
is_stderr
and isinstance(data, str)
and data.startswith('<span class="codehilite">')
)
# Use mimetype from console if available (new format)
if "mimetype" in console and console["mimetype"] is not None:
mimetype = console["mimetype"]
else:
# Backward compatibility: detect mimetype using heuristics
# HACK: We need to detect tracebacks in stderr by checking for HTML
# formatting.
is_traceback = (
is_stderr
and isinstance(data, str)
and data.startswith('<span class="codehilite">')
)
mimetype = cast(
KnownMimeType,
"application/vnd.marimo+traceback"
if is_traceback
else "text/plain",
)
console_outputs.append(
CellOutput(
channel=CellChannel.STDERR
if is_stderr
else CellChannel.STDOUT,
data=data,
mimetype="application/vnd.marimo+traceback"
if is_traceback
else "text/plain",
mimetype=mimetype,
)
)

View File

@@ -72,6 +72,30 @@ export interface components {
type: "streamMedia";
};
StreamOutput: {
/** @enum {string|null} */
mimetype:
| "application/json"
| "application/vnd.marimo+error"
| "application/vnd.marimo+traceback"
| "application/vnd.marimo+mimebundle"
| "application/vnd.vega.v5+json"
| "application/vnd.vegalite.v5+json"
| "application/vnd.jupyter.widget-view+json"
| "image/png"
| "image/svg+xml"
| "image/tiff"
| "image/avif"
| "image/bmp"
| "image/gif"
| "image/jpeg"
| "video/mp4"
| "video/mpeg"
| "text/html"
| "text/plain"
| "text/markdown"
| "text/latex"
| "text/csv"
| null;
/** @enum {string} */
name: "stdout" | "stderr";
text: string;

View File

@@ -12,14 +12,16 @@
{
"type": "stream",
"name": "stdout",
"text": "stdout message"
"text": "stdout message",
"mimetype": "text/plain"
},
{
"type": "stream",
"name": "stderr",
"text": "stderr message"
"text": "stderr message",
"mimetype": "text/plain"
}
]
}
]
}
}

View File

@@ -693,6 +693,48 @@ def test_deserialize_error_with_traceback():
assert console_output.data == tb
def test_deserialize_session_with_console_mimetype():
"""Test deserialization of a session with console output that has mimetype"""
session = NotebookSessionV1(
version=1,
metadata={"marimo_version": "1.0.0"},
cells=[
{
"id": "cell1",
"code_hash": "123",
"outputs": [],
"console": [
{
"type": "stream",
"name": "stdout",
"text": "stdout message",
"mimetype": "text/html",
},
{
"type": "stream",
"name": "stderr",
"text": "stderr message",
"mimetype": "text/plain",
},
],
}
],
)
view = deserialize_session(session)
assert "cell1" in view.cell_operations
cell = view.cell_operations["cell1"]
assert isinstance(cell.console, list)
assert len(cell.console) == 2
console_outputs = cell.console
assert console_outputs[0].channel == CellChannel.STDOUT
assert console_outputs[0].data == "stdout message"
assert console_outputs[0].mimetype == "text/html"
assert console_outputs[1].channel == CellChannel.STDERR
assert console_outputs[1].data == "stderr message"
assert console_outputs[1].mimetype == "text/plain"
def test_serialize_session_with_dict_error():
"""Test serialization of a session with a dictionary error"""
view = SessionView()