Skip to content

Commit 4aeefea

Browse files
TomNaessensclaude
andcommitted
Emit incremental turtle snapshots on sleep and fix stacking
When user code calls time.sleep(), emit the current turtle SVG so the drawing updates live rather than only appearing at the end. Deduplicate via _last_emitted_turtle_svg so identical frames are never sent twice, and apply the same dedup to _render_turtle and turtle.done()/mainloop(). Consolidate the three separate SVG-fetch-encode-emit blocks (turtle_hook render closure, debug frame_callback, _render_turtle) into the single _emit_turtle_snapshot helper. Fix the frontend regression where incremental SVGs were appended instead of replacing the previous frame by removing the debugger-only guard from the "show only latest SVG" filter in Output.ts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c561d93 commit 4aeefea

File tree

3 files changed

+30
-38
lines changed

3 files changed

+30
-38
lines changed

src/backend/workers/python/papyros/papyros.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ def __init__(
4949
self._tracking_files = False
5050
self._original_open = builtins.open
5151
self._last_emitted_snapshot = None
52+
self._last_emitted_turtle_svg = None
5253
self._install_open_tracking()
5354
self.limit = limit
5455
self.override_globals()
@@ -76,6 +77,7 @@ def cb(typ, dat, contentType=None, **kwargs):
7677
elif event_type == "input":
7778
return cb("input", data["prompt"])
7879
elif event_type == "sleep":
80+
self._emit_turtle_snapshot()
7981
return cb("sleep", data["seconds"]*1000, contentType="application/number")
8082
else:
8183
return cb(event_type, data.get("data", ""), contentType=data.get("contentType"))
@@ -90,13 +92,23 @@ def override_globals(self):
9092
self.override_matplotlib()
9193
self.override_turtle()
9294

93-
def _render_turtle(self):
95+
def _emit_turtle_snapshot(self):
96+
"""Emit the current turtle drawing if it has changed since the last snapshot."""
9497
hook = getattr(self, '_turtle_hook', None)
95-
if hook and hook.render:
96-
try:
97-
hook.render()
98-
except Exception:
99-
pass
98+
if not hook or not hook.render:
99+
return
100+
try:
101+
from svg_turtle import SvgTurtle
102+
svg_string = SvgTurtle._pen.to_svg()
103+
if svg_string and svg_string != self._last_emitted_turtle_svg:
104+
self._last_emitted_turtle_svg = svg_string
105+
img = base64.b64encode(svg_string.encode("utf-8")).decode("utf-8")
106+
self.output("img", img, contentType="img/svg+xml;base64")
107+
except Exception:
108+
pass
109+
110+
def _render_turtle(self):
111+
self._emit_turtle_snapshot()
100112

101113
def override_turtle(self):
102114
if not hasattr(self, '_turtle_hook'):
@@ -222,6 +234,7 @@ def _execute_context(self):
222234
self._tracked_files.clear()
223235
self._tracking_files = True
224236
self._last_emitted_snapshot = None
237+
self._last_emitted_turtle_svg = None
225238
with (
226239
redirect_stdout(python_runner.output.SysStream("output", self.output_buffer)),
227240
redirect_stderr(python_runner.output.SysStream("error", self.output_buffer)),
@@ -256,24 +269,11 @@ async def run_async(self, source_code, mode="exec", top_level_await=True):
256269
self.callback("start", data="RunCode", contentType="text/plain")
257270
if mode == "debug":
258271
from tracer import JSONTracer
259-
_last_turtle_svg = [None]
260272

261273
def frame_callback(frame):
262274
self._flush_open_files()
263275
self._emit_created_files(emit_empty=True)
264-
# The hook fires lazily when user code runs `import turtle`, so the
265-
# pen only exists partway through the trace — check per frame.
266-
hook = getattr(self, '_turtle_hook', None)
267-
if hook and hook.render:
268-
try:
269-
from svg_turtle import SvgTurtle
270-
svg_string = SvgTurtle._pen.to_svg()
271-
if svg_string and svg_string != _last_turtle_svg[0]:
272-
_last_turtle_svg[0] = svg_string
273-
img = base64.b64encode(svg_string.encode("utf-8")).decode("utf-8")
274-
self.output("img", img, contentType="img/svg+xml;base64")
275-
except Exception:
276-
pass
276+
self._emit_turtle_snapshot()
277277
self.callback("frame", data=frame, contentType="application/json")
278278

279279
result = JSONTracer(frame_callback=frame_callback, module_name=MODULE_NAME).runscript(source_code)

src/backend/workers/python/papyros/turtle_hook.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import base64
21
import sys
32

43

@@ -58,17 +57,10 @@ def __init__(self):
5857
SvgTurtle._screen = screen
5958
SvgTurtle._pen = PapyrosTurtle()
6059

61-
rendered = [False]
6260
papyros = self.papyros
6361

6462
def render():
65-
if rendered[0]:
66-
return
67-
rendered[0] = True
68-
svg_string = SvgTurtle._pen.to_svg()
69-
if svg_string:
70-
img = base64.b64encode(svg_string.encode("utf-8")).decode("utf-8")
71-
papyros.output("img", img, contentType="img/svg+xml;base64")
63+
papyros._emit_turtle_snapshot()
7264

7365
turtle_mod.Turtle = PapyrosTurtle
7466
turtle_mod.done = render

src/frontend/components/Output.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -98,16 +98,16 @@ export class Output extends PapyrosElement {
9898

9999
get renderedOutputs(): TemplateResult[] {
100100
let outputsToRender = this.outputs;
101-
if (this.papyros.debugger.active) {
102-
// Only show the latest SVG snapshot so the debugger slider shows progressive rendering.
103-
const lastSvgIdx = outputsToRender.findLastIndex(
104-
(o) => o.type === OutputType.img && o.contentType?.includes("svg"),
101+
// Only render the latest SVG snapshot — intermediate frames from sleep/debug are kept in
102+
// the output array (so the debugger slider can step through them) but only the current
103+
// one should be visible at any given time.
104+
const lastSvgIdx = outputsToRender.findLastIndex(
105+
(o) => o.type === OutputType.img && o.contentType?.includes("svg"),
106+
);
107+
if (lastSvgIdx >= 0) {
108+
outputsToRender = outputsToRender.filter(
109+
(o, i) => !(o.type === OutputType.img && o.contentType?.includes("svg")) || i === lastSvgIdx,
105110
);
106-
if (lastSvgIdx >= 0) {
107-
outputsToRender = outputsToRender.filter(
108-
(o, i) => !(o.type === OutputType.img && o.contentType?.includes("svg")) || i === lastSvgIdx,
109-
);
110-
}
111111
}
112112
return outputsToRender.map((o) => {
113113
if (o.type === OutputType.stdout) {

0 commit comments

Comments
 (0)