Skip to content

Runlet 0.2.0

Choose a tag to compare

@filipac filipac released this 03 Oct 01:10
· 143 commits to main since this release
5bcfae0

Runlet 0.2.0 lets AI clients run PHP with your approval, shows values inline with magic comments, profiles code with flame graphs, and runs on a Mac with no PHP installed. Website: https://filipac.github.io/runlet/

Highlights

  • No PHP? No problem. On a Mac with no PHP (no Herd, no Homebrew, no Docker), Runlet offers a one-click download of its own self-contained PHP 8.5.8 (about 26 MB, for your Mac's CPU). It's checked against a SHA-256 pinned in the app before it's installed. Your installed PHP always comes first. It includes Excimer, so Profile Run works out of the box.
  • Magic comments. End a line with //? to see its value next to the code. /*?*/ shows an intermediate value, /*?->count()*/ a projection, and /*?.*/ the elapsed time. Values stream in while the code runs, loops show ×N, and hovering shows the full value and every hit. They work on every target, with Run Selection, and can be switched off in Settings.
  • MCP server for AI clients. Claude Code, Claude Desktop, Cursor and other MCP clients can list your targets and snippets, save snippets, and ask to run PHP (runlet mcp). Every run asks you first, in a sheet showing the client, the target and all of the code. Only the sandbox can be allowed for a session, production always asks, and SSH never connects silently. It's off by default (Settings ▸ AI Clients).
  • Benchmark and profile.
    • Runlet\bench() times functions with min, median, p95, ops/s, memory and a distribution, and compares several side by side. Laravel's Benchmark::dd() gets the same card.
    • Profile Run (⌥⌘R) samples your snippet with Excimer and shows an interactive flame graph.
  • Open REPL. php artisan tinker, PsySH or php -a in a terminal tab, on any target: local, sandbox, Docker or SSH. Production asks every time.
  • Output, realtime or at once. Choose in Settings ▸ General. Large output is now fast in both modes: 5,000 dumps or 200,000 lines show in under half a second instead of freezing the window. Plain and Raw get a native text view with Find.
  • More:
    • Explain for captured SQL opens a ready-to-run tab with the query plan request.
    • String viewers: JSON trees, searchable text, images and HTML previews.
    • Run timing breakdown: bootstrap, execute, memory and queries.
    • Sandbox-only Auto-run.
    • Snippet descriptions.
    • Keep compiled PHP on the server is now on for new SSH profiles.
  • Fixes:
    • Pane dividers no longer draw behind the title bar and tabs.
    • The editor's gutter line no longer draws through banners.
    • A Metal validation assert no longer stops runs from Xcode.

Install

Download Runlet-0.2.0.dmg (drag Runlet to Applications) or Runlet-0.2.0.zip. Requires macOS 26 or later, on Apple silicon or Intel.

This build is ad-hoc signed and not notarized, so macOS blocks it the first time. Either right-click Runlet.app ▸ Open ▸ Open, or run:

xattr -dr com.apple.quarantine /Applications/Runlet.app

Checksums (SHA-256)

2702ffe07285ee5d6a3fa0dc55fcaa46fa3bc126d2281fbb4bcbfa966949fb23  Runlet-0.2.0.dmg
2888079b7418b8735bee0a2f6ffc994f07f606cb1c64ee00e64584d2c71f8b82  Runlet-0.2.0.zip

Notes

  • Early preview. Package tests passed in every pull request in this release; for this build the packaged self-test passed (6 of 6 checks). The UI test suite was not re-run.
  • Runlet's own PHP is published separately as the php-8.5.8-r2 pre-release and downloaded only when you click.
  • Free and open source under the MIT License.
Full changelog for 0.2.0

2026-10-03 — Output realtime or at once (#82)

  • Settings ▸ General ▸ Output: Realtime (the default) or At once. At once shows a
    run's printed output, dumps, result, errors, magic-comment values, and the inspector's
    queries, mail, logs, benchmarks, and profile together when it ends: completed, failed, dd(),
    exit, or stopped (what arrived before Stop is shown). The status bar, elapsed time, Stop, and
    the Run Log stay live; while it runs the output says "Output appears when the run ends". The
    app holds the output, so every target behaves the same, and AI clients over MCP get the full
    result in both modes.
  • It replaces the magic comments' Show values while the code runs switch (#10). A saved
    "off" becomes At once; older settings files open with Realtime.
  • Large output keeps up: before, 5,000 dump() calls or 200,000 echoed lines took minutes to
    appear and froze the window. The tab now takes events in batches (at most ten updates a
    second, fewer while drawing is slow), printed output is drawn a piece at a time, and the same
    runs show within a few hundred milliseconds of finishing. Structured shows the last 5,000
    lines of a printed output and the last 1,000 cards (Show All shows every card); Plain and
    Raw now use a native text view that appends new output, follows the end while scrolled to the
    bottom, and has Find. Plain, Raw, Copy Output, and Save Output always have everything.
  • New DEBUG step wait-run[:<seconds>]: waits for the selected tab's run and prints its timings.

2026-10-03 — Specialized string viewers (#7)

  • Structured strings offer JSON trees with Copy Pretty, searchable/wrapping text, PNG/JPEG/SVG images, and restricted HTML previews. Long strings open in Text; the original Tree stays available. Recognition and raster decoding are bounded, and incomplete strings retain their truncation notice. See the viewer guide.

2026-10-03 — Run timing breakdown (#9)

  • Finished output shows Bootstrap, Execute, and Started alongside labeled total time, peak memory, and query time. Hover the finished row or status to see the complete breakdown, including unavailable phases.
  • Completion events preserve runner phase timings through normal, error, cancellation, and transport-close paths. Old completion records remain readable. Query metrics survive Clear Output in the status tooltip and reset for the next run. See the timing guide.

2026-10-03 — Runlet's PHP r2 with Excimer (#79)

  • Runlet's own PHP is now build php-8.5.8-r2, which adds the Excimer extension, so Profile
    Run works on a Mac with no other PHP installed. Same PHP 8.5.8 and extensions otherwise.
  • Updating from r1: an installed older build keeps working, and Settings ▸ PHP ▸ Runlet's PHP
    shows "PHP 8.5.8 (r1) installed · Update to r2. Adds Excimer, so Profile Run works." Update
    downloads and verifies r2, moves the default PHP and projects' PHP from the old binary to the
    new one (also if Runlet quit in between), and removes r1. Nothing downloads without a click.
  • Remove now clears the default PHP and projects' PHP that pointed at any build of Runlet's PHP.

2026-10-03 — Xcode runs no longer stop on a Metal validation assert (#85)

  • Running from Xcode stopped at random on instanceCount(0) must be non-zero, raised by Metal API Validation while Core Animation replayed a line stroke with nothing to draw. Normal launches were unaffected. The Runlet scheme now runs with Metal API Validation off; turn it back on in Edit Scheme when debugging GPU issues.
  • The benchmark charts and the flame graph skip drawing at zero size, and the flame graph skips the hover outline on frames too small to show it, so Runlet's own views never ask for an empty stroke.

2026-10-03 — Magic comments (#10)

  • Magic comments show values in the editor while the code runs, without dump() calls or
    temporary variables. //? at the end of a line shows the line's value (an assignment's
    value, a return, an echo; ✓ on a line without a value, such as foreach (…) { //?).
    /*?*/ right after an expression shows that value, /*?->count()*/ (any -> or ?->
    chain) shows a projection while the code keeps the value itself, and /*?.*/ shows the
    milliseconds since the previous one (or since the snippet started).
  • Values appear as dim text after the line, ×N and the latest value for lines that run more
    than once. Hovering them (or Edit ▸ Show Inline Value) opens a panel with the value tree and
    the list of hits. Magic comments are highlighted, and the gutter marks lines whose comments
    ran. Values stream in while the code runs, on every target, and map back to the right lines
    for Run Selection. Edit ▸ Clear Inline Values and Clear Output remove them.
  • Adding magic comments never changes what the code does. The runner inserts probe calls at
    byte offsets on the same lines (it never re-prints the code), keeps references
    (by-reference arguments, =&, foreach (… as &$v), by-reference returns and yields) and
    nullsafe short-circuits, and refuses places where a call would change the code: assignment
    targets, isset()/empty()/?? operands, constant expressions, the start of "{$…}", and
    variables passed to methods that might take them by reference. Refused comments get one
    notice and a short reason on their line; the code runs as written. Semantics fixtures run
    each snippet with and without its magic comments and require the same results (PHP 8.4 and
    7.4).
  • Limits: the first 100 hits of each comment carry values, later hits are counted with a value
    sampled about four times a second, and values stop after 16 MiB per run. Projections run
    only for hits whose values are sent, and may query a database.
  • The next run clears values; a line edited since the run loses its values, and other lines
    keep theirs, moved with their text. Values never start a run.
  • Settings ▸ General ▸ Magic Comments: Show values of magic comments turned off makes them
    ordinary comments: runs get no probes on any target, and the editor neither highlights them
    nor shows values. Show values while the code runs turned off shows a run's values
    together when it ends (also after a failure or Stop); the app holds them, so the runner and
    every target work as before. Both are on by default. Profile Run never inserts probes, so
    its flame graph shows only the code as written.
  • A comment after the final expression (1 + 1; // note) no longer hides its result.
  • New DEBUG steps for screenshots: selection:<first>-<last> and inline:<line>|off.

2026-10-03 — Benchmark and profile (#41)

  • Runlet\bench($callables, $iterations = 1000, $label = null, $seconds = null) measures code
    in any snippet, on every target (PHP 7.4+, no extension): a cold call, a short warm-up, then
    up to 100,000 calls timed with hrtime(true), stopping early when a callable has used its
    time budget (1 s by default, at most 60 s). It returns the numbers in milliseconds and shows
    a benchmark card where it ran and in a new Benchmarks section: mean, median, p95, min,
    max, throughput, iterations, standard deviation, the first call, the peak memory and the
    memory kept per call, a histogram of call times with median and p95 markers, and the times
    in run order. An array of callables keyed by label is compared side by side.
  • Laravel's Benchmark::dd() shows the same card, with the averages Laravel measures: Runlet
    recognizes its dump. Benchmark::measure() only returns numbers, so Runlet\bench() takes
    the same arguments.
  • Run ▸ Profile Run (⌥⌘R, and in the Command Palette) runs the tab like Run, with the
    same production confirmation, and samples the snippet with the Excimer extension (wall
    time, every millisecond). The Profile section draws a native flame graph: hover a frame
    for its function, file and line, samples and share; click to zoom in, Reset to zoom out,
    and search to highlight frames. It also lists the hottest functions and copies the samples
    as collapsed stacks. Stacks are bounded (4,000 stacks of up to 200 frames), and the view
    says when something was folded.
  • Profile Run is disabled, with the reason, when the target's PHP doesn't load Excimer; the
    Command Palette still lists it with that reason. PHP discovery (Settings ▸ PHP), the Docker
    profile's Test, SSH Test Connection, and every run report which profilers a PHP loads. SPX
    is detected but not used: it profiles only processes started with SPX_ENABLED=1 and
    writes its reports to files. A PHP without Excimer stops a Profile Run before anything
    runs.
  • The editor no longer marks Runlet\bench() or Runlet\Inspector as unknown: the runner
    defines them when it runs.
  • Testing: a disposable runlet-fixtures profiler service (PHP 8.4 with Excimer and SPX), and
    Tests/Fixtures/docker/fixtures-only-docker, the real Docker CLI limited to Runlet's
    disposable containers, for running the app or swift test against real Docker. Debug
    builds add flame:hover|zoom|search|reset and profiles:<name> steps for screenshots.

2026-10-03 — Personal snippet descriptions (#52)

  • Save Snippet and Edit Snippet support an optional description. Descriptions appear in the Snippets panel and Open Anything, and both search them. Blank descriptions are removed; older snippet libraries load without migration.
  • Duplicate and Copy to Personal preserve descriptions. MCP list_snippets searches and returns personal descriptions, and get_snippet returns them when present. Saving, editing, copying, and opening snippets never runs their code. See the guide.

2026-10-03 — Sandbox-only auto-run (#30)

  • Sandbox tabs offer Auto-run in the toolbar. Explicitly enabling it shows AUTO and evaluates the whole tab after 800 ms without editor edits; enabling alone never runs the existing code. Changes during a run wait for completion, with no overlapping executions.
  • Auto-run is off by default and never saved in sessions or workspaces. Switching targets, loading code, reopening a closed tab, or restarting requires a fresh opt-in. Local, Docker, SSH, and production targets have no auto-run option. Stop, explicit Run, disabling auto-run, and closing the tab cancel pending automatic execution.
  • See the sandbox auto-run guide for behavior and validation.

2026-10-03 — MCP server (#43)

  • AI clients such as Claude Code, Claude Desktop, and Cursor can use Runlet through
    runlet mcp, the bundled tool started with the argument mcp. Its tools: list_targets,
    list_snippets, get_snippet, add_snippet (saves only), run_php(target, code), and
    get_last_output. Results read like the output pane: output, dumps, the result, errors
    with the line in the code sent, the duration, and the target, plus the same as
    structured data.
  • Every run_php shows a sheet in Runlet's window, brought forward, with the client's name,
    the target and where it runs, and all of the code. ⌘↩ runs; ↩ or Esc cancels, and the
    client hears that nothing ran. A request nobody answers expires after 5 minutes. A request
    the client cancels is withdrawn; a run that started finishes in its tab.
  • "Allow sandbox runs from for this session" is offered only for the Laravel
    sandbox. It lasts until that client disconnects or Runlet quits, and Settings can revoke
    it. Local projects and Docker applications always ask.
  • Production targets always ask, with a red warning and Run on Production. The 10-minute
    "don't ask again" never applies to AI clients, and approving their runs never grants it.
  • SSH hosts are never connected silently: the sheet says when pressing Run connects, and a
    host that needs a password or a one-time code is refused until you log in with Connect….
  • Approved runs open in a tab named after the client, with a note saying who asked, and
    are recorded in History. Listing, reading, and saving snippets, starting Runlet, and
    restoring tabs never run code.
  • Settings ▸ AI Clients turns the server on (it is off by default), shows its status and the
    connected clients, and gives the Claude Code command and the mcpServers JSON for this
    copy of Runlet.
  • The app listens only on a Unix socket in <data folder>/MCP (folder 0700, socket 0600),
    never on the network. Both ends check that the other runs as the same user
    (getpeereid), and messages are size-limited. RUNLET_DATA_DIR moves the socket with the
    data. If Runlet isn't running, runlet mcp starts it in the background on the first call.
  • The protocol is MCP 2026-07-28 (per-request metadata, server/discover), with
    initialize-based clients served on 2025-11-25, 2025-06-18, 2025-03-26, or 2024-11-05.
    No third-party code.
  • runlet --target (and the MCP tools) now also find SSH profiles (ssh:<name>) and take
    <kind>:<id> when two targets share a name. Opening a file on an SSH host never connects.
    runlet mcp started by hand in a terminal explains itself and exits; a folder named
    mcp opens as runlet ./mcp.
  • Debug builds: the steps mcp:on|off, mcp-wait, mcp-approve[:session], mcp-decline,
    and mcp-state; RUNLET_DEBUG_MCP_APPROVAL_TIMEOUT and RUNLET_DEBUG_APP_PATH;
    RUNLET_MCP_NO_LAUNCH for the tool. scripts/mcp-e2e/driver.py checks the whole flow
    against a hidden Debug build with scratch data. Documentation:
    docs/mcp.md.

2026-10-03 — Open REPL in the terminal (#32)

  • The Commands pane has Open REPL (also Library ▸ Open REPL and the command palette): the
    target's own interactive REPL in a terminal tab, so variables and state carry over from
    one line to the next. It works without listing commands first, and it starts only when
    clicked: opening, importing, or restoring code never opens one.
  • Runlet picks the REPL from the project's files: Tinker (php artisan tinker) when
    artisan and vendor/laravel/tinker/ exist (the sandbox and Laravel apps), else the
    project's PsySH (php vendor/bin/psysh), else PHP's interactive shell (php -a,
    titled "PHP shell"). The pane shows which one, e.g. "Tinker · php artisan tinker".
  • Every kind of target, run the way project commands are: local projects and the sandbox type
    the command into your shell in the project folder with the target's PHP; Docker profiles
    use docker exec -it with the profile's user, working directory, and TMPDIR, in the
    resolved container; the Docker sandbox a disposable docker run --rm -it; SSH hosts an
    ssh -t through the shared connection (cd to the profile's directory, the profile's
    PHP), or docker exec -it in the container step's container on the server. Docker and SSH
    targets choose the REPL on the target, in the same sh that starts it, and set the tab's
    title ("Tinker · app-prod") once they have. The tab stays open after the REPL exits.
  • SSH connects only on the click, with the usual rules: a password or two-factor host must be
    connected with Connect… first (the button is disabled until then).
  • Production targets ask before every REPL (⌘↩ confirms). The 10-minute grace for snippet
    runs never applies, and confirming a REPL doesn't grant it: once a REPL is open, every line
    typed into it runs without another question.
  • Debug builds: a new terminal:<text> step types into the selected terminal tab (\n is
    Return, \c a comma).

2026-10-03 — Explain captured SQL in a new tab (#4)

  • Added Explain to query rows, their context menus, and expanded similar-query groups. It prepares a new PHP tab with the captured run target, SQL placeholders, typed bindings, and named connection; opening or restoring it never executes it. Explicit Run keeps the normal production confirmation.
  • SQLite requests a query plan; MySQL/MariaDB and PostgreSQL use plain EXPLAIN. Laravel/Eloquent, Symfony, and WordPress use their captured database API. Custom DBAL/PDO templates require recreating the connection explicitly. Incomplete captures and unsupported drivers cannot generate a misleading plan request. See the Explain guide for connection requirements and validation scope.

2026-10-03 — Website: "No PHP? No problem." (#67)

  • The website has a new section right after the hero about Runlet's own PHP (#2): Runlet
    runs PHP on a Mac with no PHP and no Docker. One click downloads a self-contained PHP
    8.5.8 (about 26 MB), checked against the SHA-256 pinned in the app before it's installed,
    and installed PHP always comes first. A collage of four app screenshots tells the story
    (the banner, the download in progress, Settings ▸ PHP, and a run), in light and dark,
    with four numbered steps under it. The nav's Features link now starts there.
  • Copy that said the sandbox needs installed PHP or Docker now mentions Runlet's PHP: the
    sandbox card, the install steps, the requirements, and the FAQ, which also answers "Do I
    need PHP installed?".
  • scripts/website-screenshots/shoot-own-php.sh makes the collage: a Debug build on a Mac
    that seems to have no PHP and no Docker (RUNLET_DEBUG_HIDE_SYSTEM_PHP=1, a fake Docker
    CLI, a scratch data folder at a neutral path), with the release archive served slowly on
    localhost so the download shows in progress. own-php-collage.swift lays out the shots.
    render-section.swift renders a part of the page with WebKit at a given width and
    appearance, and reports anything wider than the viewport.

2026-10-03 — Keep compiled PHP on the server: on for new SSH profiles (#68)

  • New SSH profiles start with Speed ▸ Keep compiled PHP on the server turned on: New
    SSH Profile, the Profiles window's +, hosts imported from ~/.ssh/config, and hosts
    first opened from a workspace file. Runs keep PHP's compiled files in a private cache on
    the server (~/.cache/runlet/opcache, mode 0700), and edited files are still picked up.
  • Saved profiles keep their setting. A profile saved without it (by 0.1.0 or earlier, where
    it was off unless turned on) stays off, and switching it off is now saved explicitly.
  • Turn it off in the profile and Runlet writes nothing on the server. It still applies only
    to the server's PHP, not with a container step. The help text under the toggle, the SSH
    profile header ("only the compiled-PHP cache (Speed) is kept on the server"), docs/ssh.md,
    and the website's "nothing is written on the server" lines say so.
  • Debug builds: a new scroll:<accessibility identifier> step scrolls an element to the
    middle of its scroll view, for screenshots of controls low in a sheet's form.

2026-10-03 — Runlet's own PHP when none is installed (#2)

  • When no installed PHP fits, Runlet offers to download its own self-contained PHP 8.5.8
    from Settings ▸ PHP ("Runlet's PHP") or from a banner above a sandbox or local-project tab.
    It works without Herd, Homebrew, or Docker. It is downloaded only on that click, for this
    Mac's CPU only, checked against the SHA-256 pinned in the app, and must run before it is
    installed in Application Support.
  • It is only a fallback: installed PHP (Herd, Homebrew, PATH) is always preferred, and
    Runlet's PHP comes last in the list. It can also be chosen as the default or for a project,
    and removed again.
  • The build is static-php-cli's "common" extensions plus mysqli (WordPress), intl, sodium,
    and readline (scripts/php-runtime/craft.yml). .github/workflows/php-runtime.yml builds
    it for Apple silicon and Intel, and publishes it as a php-8.5.8-r1 pre-release that never
    becomes the "Latest" release.
  • The banner and Settings show the download's progress. A failed download says why, and
    the banner offers Try Again.
  • The editor's gutter no longer draws its edge line up through the banners above the
    editor (this banner, SSH, Docker, and the sandbox image): views stopped clipping to
    their bounds by default in macOS 14, so the editor's scroll view and ruler now do.
  • Debug builds: RUNLET_DEBUG_HIDE_SYSTEM_PHP=1 behaves as on a Mac without PHP, and
    RUNLET_DEBUG_PHP_URL fetches the archive from elsewhere (a CI artifact served locally)
    before the release exists; it must still match the pinned checksum. New debug steps:
    settings-tab:<name>, frame:<window>=<size>, and shot:<name>@<window>.

2026-10-03 — Pull request workflow (#65)

  • AGENTS.md describes how work reaches main: one branch per issue, a draft pull request
    opened early, incremental commits, screenshots for UI changes, and the new
    ready to review label when the work is finished.

2026-10-02 — Keep pane dividers out of the title bar (#1)

  • Clip the main window's content and its editor/output area to their bounds so pane backgrounds and dividers cannot draw behind the toolbar, window buttons, or horizontal tab strip. Applies to both horizontal and vertical tabs.

2026-10-02 — Issue-first work tracking and backlog reconciliation (#3)

  • Added repository-wide AGENTS.md instructions to track work in labeled GitHub issues before implementation or adding TODOs.
  • Reconciled release ideas against the changelog/code, archived completed scope in docs/done-next-release-ideas.md, and linked outstanding and partial scope to GitHub issues.
  • Updated the historical MVP plan/review and stale SSH command/snippet guide references; preserved historical validation evidence without claiming a fresh test run.