mirror of
https://github.com/marimo-team/marimo.git
synced 2025-12-03 13:34:58 +00:00
improvement: better tracebacks, padding, remove button for static files (#6912)
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -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
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.13
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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" &&
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -30,6 +30,7 @@ class StreamOutput(BaseDict):
|
||||
type: Literal["stream"]
|
||||
name: Literal["stdout", "stderr"]
|
||||
text: str
|
||||
mimetype: Optional[KnownMimeType]
|
||||
|
||||
|
||||
class StreamMediaOutput(BaseDict):
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user