Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ There is no unit test framework. The automated checks are `test:protocol` (plain
- Values are shown through `_glpython_safe_repr`, which writes types as learners do (`int`, not `<class 'int'>`); the program's own output keeps Python's repr.

Worker messages:
- `run` → `result`/`error`
- `run` → `result`/`error`; while it runs, `output` messages (`{id, text}`, `id` undefined for a program, the input's id for a REPL input) stream what was printed, at most 50 per 50 ms. The final message brings the whole output again.
- `run-visual` → `trace-result` (`{trace, ast, output, graphics, error, errorMessage, truncated}`)
- `repl` → `repl-result`
- `run-visual-repl` (a doctest example) → `trace-result` with `rootPath`/`sources`, or `repl-result` if the input doesn't parse
Expand All @@ -68,7 +68,8 @@ Worker messages:
- **Rendering always replays the full trace up to the current step's `step` number.** In substitution mode the scrubber moves through a filtered step list, so use list indices only for the scrubber, never as step numbers.
- **Visualizer modes:** in `box` mode every call is a `Frame` box at its call site. In `substitution` mode (the default; prop `visualizerMode`, embed `?visualizer=`), a call with `shortForm` renders as `Frame` scope `'inline'`: its return expression with parameters substituted, in parentheses in operator context.
- **Layout and scrolling:** nested boxes show their variables below their code (CSS). The scroll effect follows the active box horizontally. It sets `scrollLeft`/`scrollTop` on the visualizer view only; don't use `scrollIntoView`, which also scrolls the LiaScript page around the embed and moves the step buttons.
- **Console and Stop:** output, REPL inputs, Tests reports and notes form one transcript (`ConsoleEntry` list after `output`). While Python is busy for more than `STOP_BUTTON_DELAY`, the run button is **Stop**: `stopPython` terminates the worker and calls `startWorker` again, so the REPL namespace is gone after a stop. The embed reports this to its parent (protocol version 4, `glpython:busy`/`glpython:stop`).
- **UI strings** come in via the `t: CoreStrings` prop. `src/i18n.ts` is their single source: `coreStrings` holds German and English, and `resolveUiLocale` picks the language. The embed uses them directly; the glpython IDE imports them and adds only its shell strings. New strings have been added as *optional* `CoreStrings` keys with an English fallback, so older hosts keep compiling; add both languages to `coreStrings`. Python values, reprs and error messages are never translated.
- **Embedded/reduced layouts:** the knobs are `compact`, `capabilities` (`WorkspaceCapabilities`, including `repl`) and `command` (`WorkspaceCommand`, an immutable `{id, type}` used by editor-free hosts to trigger run/visualize). Defaults must preserve the full-workspace behavior.

**Embed protocol.** `src/embedProtocol.ts` defines the versioned message types and validators (`parseParentMessage`, `parseParentOrigin`, `GLPYTHON_EMBED_PROTOCOL_VERSION`, `GLPYTHON_MAX_SOURCE_LENGTH`), all re-exported from `src/index.ts`. The iframe (`src/embed/App.tsx`) requires an exact `parentOrigin` query param, sends `glpython:ready`/`glpython:resize`, and accepts `glpython:load`/`run`/`visualize`/`reset`. `run`/`visualize` carry the source snapshot so the parent remains the authoritative editor. Version 2 adds `files` + `mainFile` for programs of several files, version 3 adds `glpython:test` (doctests); the embed still accepts every earlier version and answers each parent in the version it last used, so older (e.g. cached) macros keep working. Modes: `eval` (with REPL), `turtle` (graphics layout), `visual` (visualizer-only, cannot exit). Reset remounts the workspace via a `key` generation counter, which also recreates the worker. Any protocol change should bump/handle the version and be covered in `scripts/check-embed-protocol.mjs`; also update `embed/demo.html` and the README.
**Embed protocol.** `src/embedProtocol.ts` defines the versioned message types and validators (`parseParentMessage`, `parseParentOrigin`, `GLPYTHON_EMBED_PROTOCOL_VERSION`, `GLPYTHON_MAX_SOURCE_LENGTH`), all re-exported from `src/index.ts`. The iframe (`src/embed/App.tsx`) requires an exact `parentOrigin` query param, sends `glpython:ready`/`glpython:resize`, and accepts `glpython:load`/`run`/`visualize`/`reset`. `run`/`visualize` carry the source snapshot so the parent remains the authoritative editor. Version 2 adds `files` + `mainFile` for programs of several files, version 3 adds `glpython:test` (doctests); version 4 adds `glpython:stop` and `glpython:busy`; the embed still accepts every earlier version and answers each parent in the version it last used, so older (e.g. cached) macros keep working. Modes: `eval` (with REPL), `turtle` (graphics layout), `visual` (visualizer-only, cannot exit). Reset remounts the workspace via a `key` generation counter, which also recreates the worker. Any protocol change should bump/handle the version and be covered in `scripts/check-embed-protocol.mjs`; also update `embed/demo.html` and the README.
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,25 @@ visualizer's error marking, worded by `errorHint` and labelled by
In the visualizer, a call with the wrong arguments fails as in Python, with
Python's own `TypeError`.

### Console and Stop

The console (`output`: «Konsole») is one chronological transcript, like Python
in a terminal: what the program printed - shown while it runs, not only at the
end - then every REPL input with its answer, every Tests report and every note,
in the order they happened. Errors and their hints stand where they happened.
Run and Reset clear it. It keeps the last 100,000 characters of a long output.

While Python is busy for more than a moment (a program, a REPL input, Tests,
a visualization), the run button becomes **Stop** (`stop`: «Stoppen»). Stopping
ends Python's worker and starts a new one: the code stays, the console keeps
what was printed and adds `stoppedNote`, and the REPL starts in an empty
namespace (earlier entries can't be visualized anymore). Hosts can stop with
the command `{ id, type: 'stop' }`, and learn when Python is busy through the
`onBusyChange` prop.

### REPL

Below the output panel, a `>>>` input line runs Python in the namespace of the
Below the console, a `>>>` input line runs Python in the namespace of the
last Run, so learners can call the file's functions directly. An expression
shows its value (`None` too, greyed out); a statement such as `a = 5` shows
nothing; a `def` runs but adds the hint `replDefinitionHint`; an error shows
Expand Down Expand Up @@ -276,7 +292,13 @@ files run as a black box. Version 3 adds `glpython:test` (same fields as
`run`), which runs the doctests of that file. The embed still accepts every
earlier version and answers every parent in the version that parent last used
(version 1 until it hears otherwise), so older parents keep working.
`glpython:ready` reports `capabilities.files` and `capabilities.tests`.
Version 4 adds `glpython:stop`, which stops what Python is running by
restarting it (the source stays), and `glpython:busy` (`busy: true|false`),
which the embed sends to a version 4 parent when Python has been busy for more
than a moment (a program, a REPL input, Tests, a visualization), so the
parent's run button can become a stop button, as the workspace's own does.
`glpython:ready` reports `capabilities.files`, `capabilities.tests` and
`capabilities.stop`.

```ts
iframe.contentWindow?.postMessage({
Expand Down
9 changes: 8 additions & 1 deletion embed/demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
type: `glpython:${command}`,
version: GLPYTHON_EMBED_PROTOCOL_VERSION,
}
if (command !== 'reset') {
if (command !== 'reset' && command !== 'stop') {
message.source = sources[mode]
message.mode = mode
}
Expand All @@ -64,6 +64,13 @@
events.dataset.inputAvailable = String(event.data.capabilities?.input === true)
send('load')
}
// Version 4: while Python is busy (e.g. "while True: pass" typed into
// the REPL), Run becomes Stop.
if (event.data?.type === 'glpython:busy') {
const runButton = document.querySelector('[data-command="run"], [data-command="stop"]')
runButton.dataset.command = event.data.busy ? 'stop' : 'run'
runButton.textContent = event.data.busy ? '■ Stop' : 'Run'
}
if (event.data?.type === 'glpython:resize' && Number.isFinite(event.data.height)) {
iframe.style.height = `${Math.max(400, event.data.height)}px`
}
Expand Down
12 changes: 10 additions & 2 deletions scripts/check-embed-protocol.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,10 @@ assert.equal(parseParentMessage({
}), null)

// Version 1 (a single source) is still accepted, without files.
assert.equal(GLPYTHON_EMBED_PROTOCOL_VERSION, 3)
assert.equal(GLPYTHON_EMBED_PROTOCOL_VERSION, 4)
assert.deepEqual(parseParentMessage({ type: 'glpython:run', version: 1, source: 'print(1)', mode: 'eval' }), { type: 'glpython:run', version: 1, source: 'print(1)', mode: 'eval' })
assert.equal(parseParentMessage({ type: 'glpython:run', version: 1, source: 'x', files: [{ path: 'main.py', source: 'x' }], mainFile: 'main.py' }), null)
assert.equal(parseParentMessage({ type: 'glpython:reset', version: 4 }), null)
assert.equal(parseParentMessage({ type: 'glpython:reset', version: 5 }), null)

// Version 2: a program of several files and the one to run.
const program = {
Expand Down Expand Up @@ -116,6 +116,14 @@ assert.equal(parseParentMessage({ ...tests, version: 2 }), null)
assert.equal(parseParentMessage({ type: 'glpython:test', version: 1, source: 'x' }), null)
assert.deepEqual(parseParentMessage({ ...program, version: 3 }), { ...program, version: 3 })

// Version 4: stopping what Python runs; not before version 4. Earlier
// messages are accepted in version 4 too.
assert.deepEqual(parseParentMessage({ type: 'glpython:stop', version: 4 }), { type: 'glpython:stop', version: 4 })
assert.equal(parseParentMessage({ type: 'glpython:stop', version: 3 }), null)
assert.equal(parseParentMessage({ type: 'glpython:stop', version: 1 }), null)
assert.deepEqual(parseParentMessage({ ...tests, version: 4 }), { ...tests, version: 4 })
assert.deepEqual(parseParentMessage({ type: 'glpython:reset', version: 4 }), { type: 'glpython:reset', version: 4 })

const readyMessage = {
type: 'glpython:ready',
version: GLPYTHON_EMBED_PROTOCOL_VERSION,
Expand Down
Loading
Loading