Repository navigation
Runlet 0.3.0
Runlet 0.3.0 adds SQL tabs: run SQL through your application's own database connection, with no credentials. Website: https://filipac.github.io/runlet/
What's new
- SQL tabs. File ▸ New SQL Tab, or open a
.sqlfile. A statement runs through the connection your application already uses: Laravel'sDB::connection(), Symfony's Doctrine, WordPress's$wpdb, or your project driver'ssqlConnection(). Results show as a sortable, filterable table with CSV export. Runlet never asks for or stores database credentials.- Completion. Keywords and functions always. With the schema, it also completes table and column names (
o.lists the columns oforders o). The schema is read when you load it, or along with your first run, and only names and types. - Run All Statements (⌥⇧⌘R). Runs a whole script in order and stops at the first error, in one transaction by default, so a failure rolls everything back.
- SQL snippets. Personal snippets, and
.sqlfiles in.runlet/snippets/that you can share with your team. - Production targets ask before every SQL run. Run All lists each statement with a warning when it can change data.
- Completion. Keywords and functions always. With the schema, it also completes table and column names (
- App Info. Click the framework chip to see where a snippet will run:
php artisan about-style details for Laravel, Symfony, and WordPress, PHP's settings, and your driver's own sections. Secrets are hidden. - Parameterised snippets.
@input int $orderId "Order ID"in a snippet's docblock asks for the value when it opens. - Tests in the Commands pane. Run the project's tests (all of them, one file, or a filter) in a terminal tab, with
php artisan test, Pest, or PHPUnit. Not available on production targets. - Production guard. When the application says it runs in production but its target isn't marked, Runlet offers Mark as Production. History remembers how each run's target was marked.
- Output pane settings (off by default): keep the pane hidden until a tab runs, and let Escape hide it. Docker profiles have Browse… for the container's directories.
- Fixes: the editor no longer opens scrolled sideways; red error lines and bracket highlights no longer stick; line numbers line up on blank lines; loaded code uses the editor font. Laravel completion handles natively typed relations and
casts().
Install
Download Runlet-0.3.0.dmg (drag Runlet to Applications) or Runlet-0.3.0.zip. Requires macOS 26 or later, on Apple silicon or Intel.
Runlet is an early release and is not notarized yet (ad-hoc signed), so macOS blocks the first launch. Open Runlet once, then go to System Settings ▸ Privacy & Security and click Open Anyway next to the message about Runlet. Or remove the download quarantine in Terminal:
/usr/bin/xattr -dr com.apple.quarantine /Applications/Runlet.appUpdating from 0.2.x: replace the app in Applications. Your snippets, history, sessions, and targets stay where they are and load unchanged.
Checksums (SHA-256)
6eeb57c1d74827b411f3b6ab681323ad7861ffdc9404c65a33c682dd378baf7f Runlet-0.3.0.dmg
1dc64b76a506570913197d9e5e3fa25ba822373e94e35de1e509d1fedf087d31 Runlet-0.3.0.zip
Notes
- Early preview. Package tests passed in each pull request. For this build, the packaged self-test passed (5 of 5 checks: resources, the
runletcommand, sandbox install, a sandbox run on host PHP, and PHPantom completion). The UI test suite was not re-run. SQL tabs were tested against SQLite; MySQL, MariaDB, PostgreSQL, and SQL Server weren't tested against live servers. - Free and open source under the MIT License.
Full changelog for 0.3.0
2026-10-03 — SQL tabs: completion (#128)
- SQL tabs complete as you type (two letters, or
.after a table or alias) and on Show
Completions: keywords, phrases (ORDER BY,LEFT JOIN,IS NOT NULL, …), and common functions
(COUNT()with the caret inside) always, with no connection; keywords follow the case you type. - With the connection's schema: tables after
FROM,JOIN,UPDATE,INTO, andTABLE; the
columns of the statement's tables first, with their table and type;alias.andtable.list
that table's columns,schema.a PostgreSQL schema's tables. Names that need quotes are
inserted quoted (backticks on MySQL). Nothing is offered inside strings, comments, or quoted
names. - The SQL bar's schema menu ("3 tables", "No schema") has Load Schema, Reload Schema, and Forget
Schema (also Load SQL Schema in the palette). Load Schema reads the schema in a fresh runner,
apart from the tab's output, and production targets confirm it every time. The first successful
Run or Run All on a connection also reads it, after the statements, except on production. - Only table and column names and types are read:
information_schemaon MySQL, MariaDB,
PostgreSQL, and SQL Server,sqlite_masteron SQLite; a callable connection tries each in turn.
Drivers can return the schema themselves with the new optionalsqlSchema(?string $connection).
At most 2,000 tables and 50,000 columns; kept in memory per target and connection until Forget
Schema, a target edit, or quitting, never saved. A schema that can't be read never fails a run. - Runner:
SqlTab::schema(), aschemaflag onrun()andrunAll(), and ansqlSchemaevent.
Debug builds add thesql-schema:load|forget|statestep.
2026-10-03 — SQL tabs: Run All Statements (#129)
- Run ▸ Run All Statements (⌥⇧⌘R), the SQL bar's Run All, or the palette runs every
statement of the selection (or the tab) in order, on one connection, in one PHP process. Each
statement gets its own result card ("Statement 2 of 5", its line and text). Runlet stops at the
first error and says which statement failed, what was rolled back, and what didn't run. - In a Transaction (the SQL bar's checkbox, on by default, saved with the tab): committed after
the last statement, rolled back when one fails. PDO connections use PDO's transactions;
callables getBEGIN,COMMIT, andROLLBACK. MySQL and MariaDB commit DDL and some other
statements at once: the output says so before running, and Runlet opens a new transaction after
each, so a failure rolls back only what came after. Scripts with their ownBEGIN,COMMIT,
ROLLBACK, orSAVEPOINTare refused while it is on. - On production, Run All asks once and lists every statement with its line and its own write
warning, and whether it runs in a transaction. Run (⌘R) still runs one statement and refuses
several. History keeps the script as one SQL entry. - Runner:
SqlTab::runAll(),SqlStatementFailed, andstatementonsqlevents. Debug builds
add thesql-run-allandsql-transaction:on|offsteps.
2026-10-03 — SQL snippets (#130)
- Save as Snippet from an SQL tab saves an SQL snippet ("Save SQL Snippet"); saving to the project
writes.runlet/snippets/<slug>.sqlwith-- @labeland-- @descriptionlines. SQL
snippets show an SQL badge and open as SQL tabs (or switch the current tab); opening never runs
them, and they have no@inputs. - Project snippets can be
.sqlfiles, listed with the PHP ones; metadata is the first run of
--comment lines with@labelor@description(or a/** */docblock). - Personal snippets store
"language": "sql"; PHP snippets don't write the key, so existing
libraries load unchanged. Duplicate, Copy to Personal, and History's Save as Snippet keep the
language. - MCP:
list_snippetsandget_snippetreturnlanguage;add_snippettakes an optional
language(phporsql).run_phpstill never runs SQL.
2026-10-03 — Production guard: app environment and history (#12)
- Runs report the environment the application says it is in, in the
bootstrappedevent:
app()->environment()for Laravel, Lumen, and Laravel Zero, the kernel's environment for
Symfony, andwp_get_environment_type()for WordPress. Plain PHP and Composer projects
report none. Only the name is read, and the Run Log's "Booted …" line shows it. - Project drivers can report it with a new optional
environment()method (no native return
type, so drivers that already had a method of that name keep loading; a throwing
environment()is a Run Log line and the run continues). Seedocs/drivers.md. - When the application says
production,prod,prd, orliveand the target isn't marked
production, the tab shows a notice: the run that revealed it didn't ask first, Mark as
Production marks the target as its settings would (the badge and confirmations apply from
the next run; nothing runs), and Dismiss hides the notice for that target, also after a
restart. A target marked production whose application sayslocal,development, ordev
gets an informational note with Dismiss only. Runlet never changes a marking by itself. - History keeps, for each run, how its target was marked (environment and colour) and the
environment the application reported. Rows show a PROD (or STAGING) badge and the colour from
that snapshot, so editing the target later doesn't relabel earlier runs; the status line and
tooltip show the reported environment, and searching History forproductionfinds them.
History saved by earlier versions loads unchanged, without badges. - Opening, importing, or restoring code still never runs it, and production confirmations are
unchanged.
2026-10-03 — App Info panels (#19)
- Click the framework chip, in the status bar or on a vertical tab card, to open App Info
for the tab's target: key/value sections that show where a snippet will run. Library ▸
Show App Info and the command palette open it too. The status bar's chip reads "App Info"
until the framework is known. - Laravel shows what
php artisan aboutshows (environment, debug mode, URL, maintenance
mode, caches, drivers, storage links, and sections packages add), read in the booted
application instead of running Artisan; Runlet doesn't runcomposer --version, so the
Composer version is left out. Lumen and Laravel before 9.21 get the same rows from the
configuration. Symfony shows its version and support dates, environment, debug, charset,
kernel, and folders; WordPress its version, environment type, URLs, theme, multisite,
plugins, debug constants, and database. Every target gets a PHP section (version, memory
limit, OPcache, Xdebug, time zone, php.ini, PDO drivers, extensions). - Drivers add their own sections with
panels(): array('Title' => ['Label' => value]),
shown after Runlet's. A failingpanels()keeps the built-in sections and shows the error.
See drivers.md;Tests/Fixtures/custom-driver/has an example. - Values that look like secrets are never shown or copied (
••••••with a lock): by label
(password, secret, token, key, salt, credential, …) and by value (credentials in URLs and
DSNs,password=pairs, Laravelbase64:keys, JWTs, private keys, known API token
formats). The rule runs in the runner and again in the app. App Info is bounded (20
sections, 100 rows each, 2,000 bytes a value, 256 KB in all), and paths are shown relative to
the project or with~. - App Info boots the application, so it loads only on that click (or Refresh), never on open,
import, or restore. The result is kept per target, with its age, until Refresh or an edit of
the target. Production targets confirm every load (⌘↩), outside the snippet-run grace; SSH
hosts are reached only then, under the usual connection rules. Boot errors and timeouts show
in the popover with Try Again. - Runner:
mode: "panels"and twopanelsevents (built-in, then the driver's); new
Resources/Runner/src/Panels.php. Debug builds add theapp-info[:card|off]and
app-info-statesteps.
2026-10-03 — SQL tabs (#35)
- SQL tabs. File ▸ New SQL Tab, Switch Tab Language (PHP/SQL) in the Window menu, the palette, and a tab's context menu, or open a
.sqlfile (File ▸ Open…, Finder,runlet query.sql). The language is kept in sessions, workspaces, duplicates, Reopen Closed Tab, and run history; files from before load as PHP. SQL tabs have an SQL highlighter and--comments, and no PHPantom (no PHP diagnostics or completion on SQL). - One statement per run, through the application's own connection. Run sends the selected statement, or the statement at the caret. A selection with several statements is refused before anything runs, and the runner prepares natively, so the database rejects a second statement too. Rows show as a sortable, filterable table (Copy/Export CSV, row copy) with the row count and time; other statements show the rows they affected. At most 1,000 rows, 200 columns, 8 KiB per cell, and 8 MiB per result; a cut result says so.
- Connections without credentials. The bar above the editor picks the default connection or a named one (the names the driver reported after a run, or any name). Runlet asks the project's driver first, then uses Laravel's
DB::connection(), Symfony's Doctrine registry, WordPress's$wpdb, or an Eloquent connection the application set up. Projects with none (plain PHP, Composer, Symfony without Doctrine) get a clear "No SQL connection" message. Runlet never asks for or stores database credentials. - Driver API.
sqlConnection(?string $connection)returns aPDO, a callable that returns rows or an affected-row count, ornull;sqlConnections()lists names for the picker. The built-in Laravel, Symfony, and WordPress drivers implement both, andRunlet\SqlConnectionshas helpers for Eloquent, Doctrine DBAL, and$wpdb. See drivers.md. - Safety. Opening, importing, or restoring an SQL tab never runs it; SQL tabs never auto-run, can't be profiled, and are never run by AI clients'
run_php. On production every SQL run asks (the 10-minute grace doesn't apply), showing the statement, the connection, and a warning when it can write (UPDATE, DDL,SELECT … INTO,FOR UPDATE,EXPLAIN ANALYZE …). Write detection is best-effort; development and staging targets don't ask. - Guide: sql-tabs.md. Follow-ups: SQL completion (#128), multi-statement scripts (#129), snippets with a language (#130).
2026-10-03 — Parameterised snippets (#14)
- Snippet docblocks can declare inputs:
@input <type> $<name> ["Label"] [= default] [{choice, …}]
with the typesint,float,string, andbool, for example
@input int $orderId "Order ID"or@input string $reason = "duplicate" {duplicate, fraudulent}.
Project snippets read them from the metadata docblock, personal snippets from a docblock at the
start of their code. Lines Runlet can't read are listed with the reason (an orange badge in the
Snippets panel, a notice in the form) and left out; nothing fails silently. - Opening such a snippet (Snippets panel, ⇧↩ Insert, Open Anything) first shows a form with one
field per input: a text field, a menu for choices, or a checkbox, starting at the default and
validated as you type (anintmust be a whole number in PHP's 64-bit range; afloatrefuses
INF,NAN, and hex). Cancel opens nothing; snippets without inputs open as before. - Open puts each value on its own line,
$orderId = 1042;, after the opening tag, docblocks, and
useimports. An assignment to the input in the snippet's opening lines is a placeholder and
gets the value in place instead of a second assignment. The tab never runs on its own; Run
and production confirmations work as before. - The literals are generated in Swift with
var_exportsemantics: ints and floats exactly as PHP
writes them (-9223372036854775807-1,1.0E+25,-0.0), strings single-quoted with\and
\'escaped, and strings with control characters double-quoted on one line ("a\nb","\x00",
"\u{202E}"). A test evaluates about 9,000 literals with a local PHP and compares the values
and thevar_exporttext. - Copy to Personal Snippets keeps a project snippet's
@inputlines. MCPget_snippetreturns a
parameterised snippet'sinputs(andinput_problems). See
docs/snippet-inputs.md.
2026-10-03 — Gutter line numbers line up on blank lines (#124)
- The line number of a blank line sat lower than the others (3 points at the default 13-point font and 1.15 line height, 4 at 17 points and 1.5), and so did the empty last line's after a blank line; an empty editor's only number sat about a point high. A blank line's only glyph is its newline, which TextKit places at the bottom of the line, and the gutter put the number on that glyph. Every number now sits on its line's text baseline, at every font, size, and line height, with soft wrap on or off; the execution-error dot and the magic-comment bars move with it.
- All numbers also sit up to half a point higher than before: exactly on the text's baseline rather than just below it.
- Debug builds:
editor-checkalso compares each line number with its line's text baseline (for a blank line, where typed text would sit): blank lines and the empty last line at two font sizes and line heights, an empty editor, soft wrap, and the Monaco and Menlo fonts with an emoji line.
2026-10-03 — Tests group in the Commands pane (#40)
- The Commands pane has a Tests group under Open REPL: Run All, File… (one test
file), and Filter… (the tests matching--filter), each in a terminal tab on the tab's
target, in the project's folder, with the target's PHP. The tab stays open after the tests
finish. Like Open REPL it works without listing commands first, and nothing runs until you
click: opening, importing, or restoring code never runs tests. - Runlet picks the runner from the project's files:
php artisan testwhenartisan,
Laravel's Collision (vendor/nunomaduro/collision), Pest or PHPUnit, andphpunit.xmlor
phpunit.xml.distexist (it starts Pest when installed, else PHPUnit); else Pest
(php vendor/bin/pest); else PHPUnit (php vendor/bin/phpunit), each withphpunit.xml,
phpunit.dist.xml, orphpunit.xml.dist. The pane names it, e.g. "php artisan test ·
PHPUnit". - Local projects and the sandbox are checked on your Mac, including the configuration's
<testsuite>folders: a project without tests shows no Tests group (the bundled sandbox
ships withouttests/). File… opens a file picker limited to the project's folder,
starting in its first test folder. Docker profiles, SSH hosts, and SSH container steps
choose in the container or on the server, say so in the pane, take File… as a path, and
explain in the tab when the project has no runner (e.g. a deploy without dev dependencies). - Filter… and the remote File… are inline prompts with a preview of the command. A file or
filter reaches the runner as one argument (--filter=<text>, quoted;./before a path
that starts with-); spaces, quotes,$, and backticks pass through unchanged. - Disabled on production targets, with the reason in the pane: "Tests can reset the
database; they're disabled on production targets." Test suites often reset or migrate the
database (RefreshDatabase,migrate:fresh). The Artisantestcommand and a
composer testscript stay in the command list and ask first, like every command there. - SSH connects only on the click, under the same rules as Open REPL (a password or two-factor
host must be connected first). - Tests:
ProjectTestsTestscovers detection for each layout, the selection script undersh
anddash, quoting throughsh,dash,bash,zsh, and the SSH wrapping, the exact
request per target kind, the production rule, and picked files outside the project. Live
runs:php artisan test --filterin the Laravel fixture and in the fixture Docker
container, and a stand-in PHPUnit on the SSH fixture (the filter arrives as one argument,
in the profile's directory).
2026-10-03 — Code loaded into an empty tab uses the editor font (#114)
- Code loaded into an empty editor (a new tab, then Open, a History or Snippets entry, or a file reload) was drawn in the system's proportional font (Helvetica), with tab stops and line heights that did not match the line numbers, until the editor settings were applied again. Text put into the editor now always gets the editor's font, line height, tab stops, and color, whatever the editor held before.
- Undo no longer brings text back in the font, size, or line height it had before a settings change.
- Debug builds:
editor-checkalso checks loading, inserting, and reloading into an empty editor, loading after a settings change, inserting at the end, and undo and redo, comparing each character's attributes and the line heights with a newly opened editor's.
2026-10-03 — Workflow: in progress label on issues (#125)
AGENTS.md: opening a draft pull request adds thein progresslabel to the issues it will close; the label comes off as soon as no agent works on the issue (when the pull request is ready for review, or when the work stops).
2026-10-03 — Laravel completion: native-typed relations and casts() without a trailing comma (#55)
- Relations declared with only a native return type (
public function posts(): HasMany, as
make:modeland the Laravel docs write them) complete the related model:
$user->posts->first()->offers Post's attributes and relations, andposts()is
HasMany<Post>. PHPantom 0.10.0 found the related model only for relations with a generic
@returnor with no return type. - The last entry of a
casts()array without a trailing comma is read, so its attribute
completes with the cast type (source: cast). PHPantom 0.10.0 dropped it. - How: when the language server starts, Runlet reads the project's model sources and opens
adjusted copies in memory under the files' own paths (the return type blanked to spaces, the
missing comma added), only for files with one of these two shapes. Nothing is written to the
project. A model edited on disk is seen after Restart Language Server, as before. - Macros registered in a service provider's
boot()were already offered when the provider is
registered (bootstrap/providers.php, as in every Laravel 11+ app). The earlier
"unsupported" result came from a test project without that file; the tests now cover it. keyBy()andgroupBy()on Eloquent collections still lose the model type. It is a PHPantom
bug with no workaround in Runlet; the report draft is indocs/compatibility.md, and the
follow-up is #117.
2026-10-03 — Bracket-match highlight no longer sticks after typing (#113)
- Typing after an opening bracket, or deleting one, could leave the other bracket of the pair highlighted until that character was deleted. The editor now removes the previous pair's highlight wherever the edit moved it, so only the pair at the caret is highlighted.
- The bracket match and the failed line's red (#87) are each found through a marker attribute of their own, looked up only where every edit has moved them, not across the whole document.
- Debug builds:
editor-checkalso checks typing after a bracket, moving the caret, deleting a bracket, undo and redo, and edits elsewhere in the text.
2026-10-03 — A failed line's red background no longer sticks (#87)
- When a run failed, the line's red background could stay after you edited above the line, typed inside it, deleted it, or used undo and redo, even after later successful runs, sometimes on part of a line that never failed. The editor now clears it wherever the edits moved it. Bracket matches and syntax colors stay as they were.
- Debug builds: the
editor-checkstep (RUNLET_DEBUG_STEPS) checks these cases on an off-screen editor and printsRUNLET_DEBUG_EDITOR_CHECKlines.
2026-10-03 — Output: hide until a run, Escape hides (#60)
- Two switches in Settings ▸ General ▸ Output, both off by default, so nothing changes unless you
turn them on. - Hide the output pane until a run: a tab that hasn't run shows the editor alone. Starting a
run (Run, Run Selection, Profile Run, an approved AI client run, or sandbox auto-run) shows the
pane right of or below the editor, where it was and at its saved size. Clear Output on a tab
that isn't running hides it again, and switching tabs shows or hides it with the tab. Show/Hide
Output Pane (⌃⌘O) shows or hides it for the current tab. This is per tab and never saved:
opened and restored tabs start hidden, and nothing runs when code is opened or restored. - Escape hides the output pane: Escape in the editor hides the tab's output pane until its
next run or Show/Hide Output Pane, without changing the saved Show/Hide setting. Completions,
hover and signature popups, and inline-value panels still close first, a visible find bar or
text being composed keeps Escape, and ⌘. still stops a run. The palette, sheets, and the
terminal have their own focus, so Escape there is unchanged. - Neither switch moves or resizes the pane: the saved layout and divider position are used as
they are. Show/Hide Output Pane and Move Output Right/Below work as before. - Debug builds have an
editor-key:<key>step that hands a key press to the current tab's editor
while Runlet stays in the background, for screenshots and checks.
2026-10-03 — Browse directories in Docker profiles (#62)
- Docker profiles have Browse… next to the working directory, as SSH profiles do. It opens
the folder picker on the container selected in the list: path field, breadcrumb, up and home,
Laravel/Symfony/WordPress/Composer/.runlet badges, and hidden folders on request. - It lists folder names only, with a read-only
php -rrun bydocker execas the profile's
execution user with its PHP, so a folder that lists is one runs can open. It runs only when you
click Browse… or open a folder. Opening or editing a profile still lists containers without
exec'ing into any of them, and saving runs no snippet code. - It lists only in the selected container. That container is checked again before every
listing, and the profile's identity rules apply: a recreated Compose service is followed with a
notice, as runs follow it. Several replicas, a recreated container known only by its name, or
a stopped or removed container are errors that send you back to the container list. - Errors are explained: a folder the user can't open (permission denied), a missing folder, a
file instead of a folder, a missing PHP or execution user, and a container that isn't running.
A subfolder the user can't open is marked with a lock and can't be chosen. - Paths are kept exactly as chosen: absolute, with
.and..resolved by name and symlinks
(such as Debian's/bin → usr/binor a Forge-stylecurrent) kept, not resolved. - The SSH profile's container step uses the same container listing, so its errors now name the
container and the user ("doesn't exist in …"). - DEBUG steps
browse:<path>andbrowse:select:<folder>drive the open folder picker for
screenshots.
2026-10-03 — Docker tests touch only runlet-fixtures containers (#80)
- A plain
swift testno longer lists, inspects, or execs into the developer's own
containers.TestSupport.docker, the Docker CLI of every package test that runs Docker,
now runs the real CLI only throughTests/Fixtures/docker/fixtures-only-docker, which lets
through only therunlet-fixturesandrunlet-fixtures-recreateCompose projects and
Runlet's own sandbox containers. It no longer takes whateverdockeris first onPATH.
The tests find the real CLI as the app does (PATH, then the usual install folders);
RUNLET_REAL_DOCKERpicks another. There is no opt-out, since no test needs other
containers. TestSupporthands the wrapper to Docker through a small generateddockerlauncher. Tests
that run the CLI's executable themselves (in a terminal, aProcess, or a wrapping script)
go through the wrapper as well.- The wrapper is stricter. A container argument must be a full ID, a name, or an ID prefix of
one of those containers, and the wrapper passes Docker the full ID. Docker therefore can't
resolve a short ID to another container that has that name. The wrapper no longer asks
Docker whether some other container exists. Toinspect, another container doesn't exist
("No such object", as for one that is gone), andexec,cp,pause,unpause,kill,
andrmrefuse it. Each call lists the allowed containers once, with label-filtered
docker psonly.cptreats absolute and./paths as local, as Docker does. The wrapper
refuses to wrap itself, for example through an olddockersymlink to it onPATH. FixturesOnlyDockerTestsproves it with a recording stand-in for Docker
(Tests/Fixtures/docker/recording-docker) placed behind the wrapper the same way. The
stand-in has a fixture, a sandbox container, a container of another Compose project, and
one named like the fixture's short ID. Discovery, profile resolution,inspect,exec
(probes, PHP version, runs),cp,pause,kill, andrmnever hand Docker another
container. Everypsit gets is label-filtered, and other commands are refused. The tests
also check thatTestSupport.dockeris the wrapper.
2026-10-03 — Editor no longer opens scrolled sideways (#78)
- A tab now opens with column 1 just right of the line-number gutter. Before, with soft
wrap off and a line wider than the editor, a restored tab (and any tab opened with its
code, such as Explain, history, or snippet tabs) started scrolled right by the gutter's
width, so the start of every line was hidden until you scrolled back or moved the caret. - Cause: the editor's scroll view extends the clip view under the gutter and sets the clip
view's left inset to the gutter's width while laying out, but AppKit doesn't move the
scroll position with it. A new editor's first layout left the position at the old edge.
The same thing hid about a column when the gutter widened past 99 lines.clipsToBounds
(#64) wasn't involved: the bug shows without it too. - The editor's scroll view (
EditorScrollView) now keeps the distance from the text's
leading and top edges when those insets change. A position you scrolled to yourself is
kept. The workaround that scrolled generated Explain tabs to their start (#4) is gone. - Regression check:
scripts/check-editor-scroll.sh <Debug Runlet.app>opens long files in
a fresh launch, then restores them, switches tabs, and changes the tab layout and window
size, hidden and with scratch data. After each change, the new DEBUG stepeditor-scroll
prints every loaded editor's horizontal offset, and all of them must be 0.
2026-10-03 — Install steps: call /usr/bin/xattr by its full path (#105)
- The first-launch command is now
/usr/bin/xattr -dr com.apple.quarantine /Applications/Runlet.appin the README and on the website. A Pythonxattrfrom pip, pyenv, or Homebrew earlier onPATHdoesn't support-r.