Conversation
Reuses the existing reactive runtime under a new full-screen terminal UI: line-numbered editor with syntax highlighting, mouse support (click to focus, drag to select, wheel to scroll), examples picker, and inline //➜ output comments — feature parity with the web editor for echo-based notebooks. - terminal/screen.js: raw-mode I/O, SGR mouse + key parsing, diffing grid - terminal/buffer.js: text buffer with cursor-aware change-spec mapping - terminal/highlight.js: regex-based JS highlighting with output/error tints - terminal/app.js: layout, render loop, runtime wiring, modals - terminal/cli.js: executable entry point (bin: recho, script: pnpm tui) Two minor adjustments to keep the runtime importable from plain Node 24: strip TS parameter properties from BlockMetadata.ts and add explicit .js extensions in runtime/controls/index.js. All 114 existing tests still pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…e UI
Examples like abacus that pull in d3 fail in Node ("document is not
defined"). The Observable runtime's variable-rejection path called
console.error directly, which scribbled raw stack traces into the
alt-screen and corrupted the layout. Process-level error handlers were
also over-eager and would tear down the whole TUI on async failures from
notebook code.
Now:
- console.{log,info,warn,error} and process.stderr.write are captured
while the alt-screen is up and routed into a bounded message log.
- Consecutive identical errors are coalesced with a ×N badge so a
generator firing 100 rejections doesn't spam the log.
- uncaughtException / unhandledRejection are logged to that same buffer
instead of killing the UI.
- New ^L "Console" modal shows the full captured log with timestamps,
error/warn tags, scroll, and Backspace-to-clear; the status bar shows
an unread-error badge and the latest error summary.
- On clean exit we restore the originals and replay the most recent
errors to stderr so the user isn't left wondering what failed.
Also: only LF-Enter (10) needs the special name; ^L (12) is now a free
binding for the console keymap.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
An infinite loop in a cell used to freeze the TUI forever — user input,
rendering, and the runtime were all blocked by a synchronous `while (true)`.
Now `safeEval` compiles the cell function with `vm.runInThisContext` and
calls it through a tiny pre-compiled wrapper script with `timeout` armed.
A synchronous infinite loop is interrupted after 1000ms (configurable
via `createRuntime(code, { cellTimeoutMs })`), surfaced as a friendly
`TimeoutError`, captured into the message log, and surfaced inline as
//✗ in the buffer. The host realm is reused intentionally so primordials
(Object, Array, Promise) match the rest of the runtime — sharing a fresh
vm context produced `[Object: null prototype]` in inspector output and
broke existing snapshots. We trade isolation for liveness; the user is
running their own notebook, so the goal is keeping the editor alive,
not security.
Async hangs (timers, awaits) are *not* caught — those need a worker
thread; this commit covers the most common shape of the bug.
Status indicator upgrades:
- title bar shows a Braille spinner while running, ● success / ● error /
⏱ timed out / ○ idle once settled, plus run elapsed time;
- run state settles after a 500ms-quiet window (no new changes, no new
errors);
- timeouts are detected by sniffing the captured error and shown
distinctly from generic errors.
All 114 existing tests still pass (TZ=America/New_York). PTY test
confirms an infinite-loop cell now times out, the TUI recovers, and a
follow-up cell runs successfully.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…52c3c7 # Conflicts: # pnpm-lock.yaml # runtime/index.js
The runtime worker loads runtime/worker.ts directly as a worker-thread entry, and the terminal CLI runs terminal/cli.ts with plain node — both rely on Node's built-in type stripping, which Node 20 lacks (the two workerRuntime tests time out there). Bump CI to the current LTS. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The examples picker painted each list row one column too wide (its last cell landed on the right border), drew unselected rows with the terminal default background instead of the box interior background, and wrote the help line on the bottom border row — so the list visually punched through the frame. Rows now span exactly the box interior with the interior background, long names and the filter query are truncated to fit, and the help line moved to the last interior row. Same border-row and overflow fixes applied to the input prompt modal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
runtime/index.js is shared between the TUI worker and the browser editor; the static `import vm from "node:vm"` made webpack fail the Next.js build (Vercel deploy) with an unhandled node: scheme. Fetch vm via process.getBuiltinModule (Node 22.3+) instead — synchronous in Node, absent in the browser, where safeEval already falls back to new Function. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
runtime/index.js is bundled for the browser, so node:vm has no business in it — not even behind process.getBuiltinModule. createRuntime now takes a compileCell hook (default: plain new Function, the browser path), and the vm machinery — timeout-armed execution, CellTimeoutError, the realm rationale — lives in the new Node-only runtime/vmEval.js, which only the TUI worker imports and injects. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The render loop ticked at a fixed 80ms, capping animated notebooks at ~12.5fps and adding up to 80ms of typing latency. The tick now self-schedules: ~60fps while runtime changes streamed in within the last 250ms, back to the lazy 80ms once the stream goes quiet. Keystrokes render immediately after being handled instead of waiting for a tick, and the run spinner is time-gated so the fast cadence doesn't speed it up. Renders cost ~0.2-0.4ms, so the fast cadence stays well under 5% of a core. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pnpm perf boots the real App with the real worker runtime, runs an animated example (matrix-rain by default), and measures the full change→render pipeline: worker change throughput, render cadence and cost, and terminal bytes written. Generous sanity gates catch regressions in kind — a stalled animation, renders lagging the change stream, expensive frames, or runtime errors — without flaking on machine load. Kept out of pnpm test since timings are load-dependent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The palette was hard-coded for dark terminals, so on a light background the dim grays and pale token colors were barely readable. The CLI now detects the background before the first paint — RECHO_THEME env override, then an OSC 11 background-color query (luminance decides), then COLORFGBG, defaulting to dark — and setTheme swaps the palette in place. The light palette uses dark-on-light analogs tuned for contrast (e.g. the pale-yellow number color becomes mid-brown). Header, status bar, modal interior, and selection colors moved from hard-coded indices into the palette so both themes cover all chrome. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two complementary changes. The runtime now trims every output-block replacement to the span that actually differs from the current text (identical frames dispatch nothing, and the worker skips the IPC for empty batches), computed after block metadata so effects are untouched. And the TUI buffer restores a cursor or selection anchor that sits inside a replaced range to the same row/col relative to that range, instead of clamping it to the range end — so the cursor can rest inside a live //➜ block while animations rewrite it every frame. Verified end-to-end: with matrix-rain running, a cursor placed mid-block holds its exact position across 2s of frames while the line under it keeps animating; pnpm perf shows unchanged throughput (14 changes/s, ~23 renders/s, 0.7ms avg render). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
//➜ and //✗ lines get a faint background band across the editor width (dark: 234, light: 255) so runtime output reads as distinct from source. Painted before the cursor-line overlay so the cursor line stays visible inside output blocks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The header's top-right corner shows the runtime change rate (batches in the last second) while an animation is producing output — matrix-rain reads 15fps, matching its interval. Entries age out within a second, so the indicator disappears when the notebook goes quiet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The gutter grows one column (GUTTER 5 → 6) to fit a │ divider between line numbers and code, in the border color; filler rows draw it too. All coordinate math (mouse mapping, clipping, scroll) keys off the GUTTER constant, so it follows automatically. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pearmini
self-requested a review
August 31, 2026 20:44
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a Node-based terminal interface for editing and running Recho notebooks.
Changes:
- Adds the ANSI TUI, worker runtime, themes, help, file actions, and console.
- Extends runtime compilation and Node module loading.
- Adds terminal tests, dependencies, type checking, and Node 24 CI.
Reviewed changes
Copilot reviewed 39 out of 42 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
terminal/app.ts |
Implements the TUI application. |
terminal/buffer.ts |
Adds document and cursor management. |
terminal/cli.ts |
Adds the CLI entrypoint. |
terminal/docs.ts |
Loads terminal help content. |
terminal/highlight.ts |
Adds syntax highlighting and palettes. |
terminal/screen.ts |
Implements terminal input and rendering. |
terminal/theme.ts |
Detects terminal themes. |
terminal/workerRuntime.ts |
Proxies runtime workers. |
runtime/worker.ts |
Adds the worker entrypoint. |
runtime/vmEval.js |
Adds timeout-controlled evaluation. |
runtime/index.js |
Adds compiler injection and change trimming. |
runtime/stdlib/index.js |
Adds Node module loading. |
runtime/controls/index.js |
Adds explicit module extensions. |
editor/blocks/BlockMetadata.ts |
Makes fields type-strip compatible. |
types/d3-dispatch.d.ts |
Adds dispatch declarations. |
package.json |
Exposes the CLI and updates tooling. |
pnpm-lock.yaml |
Updates dependency resolution. |
eslint.config.mjs |
Includes terminal TypeScript. |
.github/workflows/test.yml |
Moves CI to Node 24. |
.gitignore |
Ignores TypeScript build metadata. |
test/workerRuntime.spec.js |
Tests worker output and restart. |
test/terminalTheme.spec.js |
Tests theme behavior. |
test/terminalScrollbar.spec.js |
Tests scrollbar behavior. |
test/terminalHighlight.spec.js |
Tests terminal highlighting. |
test/terminalHelp.spec.js |
Tests help navigation. |
test/terminalFileActions.spec.js |
Tests file operations. |
test/terminalCursorStability.spec.js |
Tests runtime edit mapping. |
test/stdlib.spec.js |
Tests Node module loading. |
test/perf/tui-perf.mjs |
Adds a TUI performance harness. |
test/js/random-histogram.js |
Uses granular D3 modules. |
test/js/matrix-rain.js |
Uses granular D3 modules. |
app/examples/sorting.recho.js |
Updates D3 imports. |
app/examples/random-histogram.recho.js |
Updates D3 imports. |
app/examples/phases-of-the-moon.recho.js |
Updates D3 imports. |
app/examples/moon-sundial.recho.js |
Updates D3 imports. |
app/examples/ml5-handpose.recho.js |
Updates D3 imports. |
app/examples/matrix-rain.recho.js |
Updates D3 imports. |
app/examples/fire!.recho.js |
Updates D3 imports. |
app/examples/animals-isotype-chart.recho.js |
Updates imports and documentation. |
app/examples/abacus.recho.js |
Updates D3 imports. |
app/docs/getting-started.recho.js |
Updates import documentation. |
app/docs/aynchronous-operations.recho.js |
Updates the D3 example. |
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+24
to
+28
| function nodeImport(name) { | ||
| if (typeof name !== "string") return Promise.resolve(name); | ||
| let module = nodeImportCache.get(name); | ||
| if (!module) { | ||
| module = import(name).then(normalizeModule); |
Comment on lines
+28
to
+30
| "bin": { | ||
| "recho": "./terminal/cli.ts" | ||
| }, |
The settle check compared total console length against the index recorded at run start, so any console.log or warn during a run made a successful notebook report as failed. lastErrorAfter already finds error-level entries since the run began — use its result directly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rename moved the file first and wrote the buffer second, so a failed write reported an error after the original path was already gone, and cross-device targets failed outright. Save to the target first, then remove the old path, and report a leftover old file distinctly from a failed rename. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Browsing an example assigned the bundled file's path to the document, so a later ^S silently overwrote the installed example and Rename could move it out of the package. Keep the buffer untitled and carry the example name as a suggestion for the save prompt and title bar. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
parseInput had no carry-over state, so a CSI, SS3, or SGR mouse sequence split across data events was lost or leaked its tail into the buffer as text. It now returns the unconsumed suffix, which onInput prepends to the next chunk. A trailing lone ESC is held briefly and flushed as a real Escape press if no continuation arrives. Modal routing moved from per-chunk to per-event so keys after a modal opens mid-batch reach the right handler. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An exception inside the tick callback became an uncaughtException, which the process handler swallowed — so the loop never rescheduled, leaving a frozen alternate screen with raw stdin. The tick now guards its body: a stray failure is logged to the console panel and the loop keeps going, while three consecutive failures (a broken render path) escalate to onFatal so the terminal is restored before exiting. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.