Conversation
The camera support probe encoded nothing, so it never produced an AV1 OBU to judge; give it a canvas it can actually encode, and hold the live stream to the same rule the probe applies. On the output side, stop re-routing the speaker at a sink it is already playing through, and size the jitter buffer for that sink rather than for the link alone. MPRIS players gain a position bar with a draggable handle.
A new git request family reports branches and worktrees with the same paging and cancellation discipline as the existing reads, plus the client bindings to call it. The opcode range gate now ends at the family's last opcode, so a new one is not dropped before a handler sees it.
A native child is spawned onto a Wayland socket bound for it alone, and the compositor stamps every surface arriving there with that application's identity — self-asserted app_id stays separate from the stamped one. A client can ask which session it is attached to, and read the environment a session was given, both refusable by policy (BLIT_ENV=0) rather than by looking like an old server.
Extensions move out of the SDK's examples directory into their own cargo workspace that hashes the same wherever the tree lives, built as a declared task so CI gets its tools, and published in the release where a digest pin can find them. `ext run` takes a URL whose fragment pins the digest it is expected to carry, and resolves a transient extension by the name `ext list` prints. The dev stack serves its own extensions to its own UI.
The systemd extension watches units from inside the server and publishes what it sees over a channel, paging the journal from that same channel and following it live, queueing what a slow reader cannot take yet. It survives the PATH a systemd unit actually has, and says so out loud when it cannot.
The session extension reads desktop entries, starts what a session asks for, and supervises it: process output is acknowledged, children are adopted rather than respawned, and what exited cleanly stays exited. An application can be started without being adopted, and discarded rather than merely disabled.
A remote gets a Control panel instead of an expanding row, with its extensions, systemd units and session applications as panels under the remote itself, and its applications shown next to the clients watching it. Units filter the way logs do. The browser can read a channel, which is what the systemd and extension views are built on.
A branches panel in the dock, keyboard-reachable and hash-addressable, that lets the dock finish switching roots before it redraws.
The Extensions panel derived the registry from the UI's port, three up. A dev page served over a tunnel has no port to offset from, and the registry binds loopback and is published nowhere, so those sessions silently asked install.blit.sh instead. The dev server now proxies /ext to its own instance's registry, which keeps a second stack's page on that stack's modules and needs no CORS. Co-Authored-By: Claude <noreply@anthropic.com>
The journal pane was one 200-line page with Older and Newer buttons: nothing arrived on its own, and reading either direction meant pressing something. The watcher has served a live `journalctl --follow` since it was written -- resumed from a cursor, batched, cancellable -- and the browser simply never opened it. It does now, from the cursor the loaded page ends on, so the join has neither a gap nor a repeat. Scrolling to the top pages history in; scrolling to the bottom pages forward only when nothing is tailing, since a tail already owns that edge. Autoscroll follows only a reader already at the bottom, so scrolling back to read does not fight the entries still arriving. Routing the tail is the part with a trap: a follow batch is shaped like a page and carries `last` on every one of them, so without taking it before the page router it would settle a query that asked nothing. The buttons are gone, and so is the line count -- the only number this side could offer was how many rows happened to be buffered. Co-Authored-By: Claude <noreply@anthropic.com>
"Control" named the button, its dialog and its subtitle. Manage says the same thing about a panel whose tabs install extensions, start applications and stop units, and it does not read as the verb for driving a machine remotely. The spec that opens it also asserted the extension registry as the page's port plus three, which stopped being true when the registry moved behind the dev server's /ext proxy. It now asserts the registry this page can actually reach -- the origin's /ext under `vite dev`, the published one from the gateway's production bundle -- because both harnesses run this file, and only one of them ever has. Co-Authored-By: Claude <noreply@anthropic.com>
A modifier reaches a Wayland app only as its own key press. Nothing else in the path carries the state: the browser sends evdev keycodes with no modifier field, the compositor sends `wl_keyboard.modifiers` solely as a side effect of a modifier key transition, and it sends it only to whoever holds focus at that moment. So Ctrl going down while a terminal pane has focus is a fact the surface focused next can never learn, and the Ctrl+K aimed at it arrives as a bare k. Both halves were missing. The compositor now restates `mods_depressed`/`mods_locked` right after `wl_keyboard.enter`, the way it already replays a missed `wl_pointer.enter` at bind time. That covers focus moving between clients, the popup grab paths, and the scale-change leave/enter -- all of which used to drop the modifier state on the floor. The canvas now replays, ahead of the key it qualifies, any modifier the browser reports held that it never forwarded a press for. A replay has to pick a physical side (left, as the synthesised chords do) because the browser's flags do not say; the matching release then arrives on whichever side was really down, so `handleKey` redirects a modifier key-up onto the twin it is holding rather than dropping it as an orphan. The event's own key is never replayed, and the paste chord's deferred Ctrl and macOS's held-back Alt keep ownership of their keys. `crates/compositor/tests/keyboard_modifiers.rs` drives a real wayland-client through it; four of its five cases fail without the compositor change. The paste tests take a shortcut -- they synthesise Ctrl+V with no physical Ctrl key-down -- so they now see the replay supply it. Co-Authored-By: Claude <noreply@anthropic.com>
Ctrl+click and Shift+click open a link in a new tab or window, and the app reads that modifier from its key press alone. But clicking is also how a surface takes focus, so on the click that focuses one the modifier is always a key this canvas never saw go down -- and the app sees a plain click. `sendPointerAt` already focuses the keyboard target on pointer-down, ahead of the button, and already flushes a pending Alt there for exactly this reason. Do the same for the modifiers the browser reports held: the focus leads (an unfocused client is told no modifiers at all), then the modifier, then the button. `syncModifiers` takes the four flags both event kinds carry; only a key event exempts its own key from the replay. Co-Authored-By: Claude <noreply@anthropic.com>
The panel listed applications as names, which is the one thing a desktop launcher never does: a Steam library of a hundred and thirty entries is a wall of identical rows, and the row you want is recognised by its artwork long before its name is read. Icons are asked for rather than pushed. The catalog is a thousand names in a few tens of kilobytes; the same thousand icons are tens of megabytes, so the panel requests the dozen rows it is drawing and the extension answers one message per id -- with the `icon` field omitted when there is none, because a silent omission would be indistinguishable from a reply still in flight. `icon.rs` resolves an `Icon=` value the cheap way: the best-sized file of that name anywhere on the icon path, rather than the spec's theme-inheritance search, which would mean parsing every `index.theme` before the first icon appeared. Two shell round trips per batch, one stat sweep and one base64, ranked scalable-first and then the smallest raster at or above 128px. The request separator is a newline, not a space: a desktop-entry id is a filename, and Steam alone installs hundreds with spaces in them. Where there is no artwork -- no `Icon=`, or a name no installed theme has -- the row keeps a monogram tile of the same size, so the list has a left column either way and does not reflow as the icons arrive. Co-Authored-By: Claude <noreply@anthropic.com>
The list of installable applications was hidden behind the filter box: with nothing typed it said "132 installed — type to search", which asks a viewer to name what they want before being shown that it exists. A launcher shows its shelf. So the list is always the whole catalog, in a scroller of its own — its own rather than the overlay's, because it is the only unbounded thing on the panel and letting it lengthen the dialog would scroll the search box, the one control for a nine-hundred-row list, off the top. The filter now narrows a list that is already there, and the twelve-row cap and its "N more — narrow the search" footer are gone with it. Artwork could not follow it. Nine hundred rows is tens of megabytes of icons to draw a dozen tiles, so a catalog row asks for its own only once it is within a screen of the viewport, and the observer's batching is what makes a scroll one request instead of one per row. The managed set is small and always visible, so it still asks outright. The observer is rooted at the viewport rather than at the list's scroller, which would read better and cannot be done: a child's `ref` runs before its parent's, so the rows would find no observer and each ask outright — the storm this exists to prevent. It is the correct root regardless, since an intersection is clipped by every scrolling ancestor on the way up. Co-Authored-By: Claude <noreply@anthropic.com>
Three faults, one symptom: icons appeared at the top of the list and nowhere below it. The lookahead was not one. `rootMargin` grows the *root's* rectangle and nothing else, and the observer was rooted at the viewport while the list clips to its own overflow — so a margin of three hundred pixels bought exactly zero rows of warning, and every batch was requested only once its rows were already on screen. Rooting it at the list is what the margin needed, and the reason it was not: a child's `ref` runs before its parent's, so the rows found no observer. Deferring the attachment to `onMount` settles that, and the lookahead is now fifteen hundred pixels — several screens, because a round trip costs a child process whatever it asks for. Each of those round trips was two children, not one. Searching and reading are now a single pass: walking the ranked directories in order means the first file that exists is the one the ranking would have picked, so there is nothing to report back and ask about. Expanding the icon path's globs is the half that does not depend on the question, so it happens once and is cached. And resolving no longer refreshes the catalog first — the ids came out of the catalog the panel already holds, so that read could only ever add a random half-second stall in the middle of a scroll. What did arrive could still be lost. The supervisor drops icon replies past a queue bound, and the panel counted every id it had ever asked about as answered, so a dropped reply left that row a placeholder for the life of the channel. Requests now expire, rows stay observed rather than being released once asked, and the batch is large enough that an ordinary scroll never reaches the bound. Measured on a 211-entry catalog, scrolling a screen at a time: before, 0 to 2 of the 12 rows on screen had artwork and it took two seconds to settle after stopping. Now it is 11 or 12 throughout, with 54 icons already in hand before the first scroll. Co-Authored-By: Claude <noreply@anthropic.com>
A remote's Session and systemd tabs exist only while their extension is serving, and that was sampled once per expansion by connecting to the channel and hanging up. A photograph cannot answer the question the panel asks: install an extension and the tab it should add never appeared, remove one and its tab outlived it until the row was collapsed and reopened — with the panel that installs extensions one tab away. So the registry is now followable. CHANNEL_WATCH declares up to 32 names on an unused client-created channel ID and is answered at once with NAMES, the declared names which have a listener; NAMES repeats whenever that answer changes, and UNWATCH releases the ID. A watch names what it follows rather than asking for the whole registry, so a watcher's traffic is bounded by its own request and cannot scale with churn it never asked about — every extension attempt mints and drops a command listener — and republication is suppressed when the answer is byte-identical. The bit is its own (26) rather than part of FEATURE_CHANNEL: a WATCH an older server does not understand is dropped by the family's unknown-kind skip rule, which reads exactly like a name nobody serves. A client that cannot see the bit keeps probing, which is all such a server can offer. Every name claim and release already funnels through two entry points, so that is where watches are answered from — including watches held by endpoints the requester has never heard of. A name is released when the owning endpoint closes, which for an extension is a beat after the control call that disabled it returns, so the tab goes away with the teardown rather than with the request. Applications is now Session, and the strip is ordered so that the two tabs every server has come first and the two an extension provides follow: the set grows and shrinks at the end of the row instead of shuffling what the viewer was aiming at. Co-Authored-By: Claude <noreply@anthropic.com>
The switcher could reach every terminal, pane, surface, layout, file and symbol, but not the applications the machine actually has. Starting one meant opening a remote's Manage panel and finding it in a list. It is now a section of its own, filtered as you type, and choosing one runs it on the spot. Start, not enable: this is trying an application, not adopting it for every future session. It appears in the Manage panel afterwards as a running row that is not enabled, which is where it can be kept or discarded. Applications already running are left out — an entry that would restart something already up is not what is being offered. Every connected server contributes, each row subtitled with the server it belongs to when there is more than one, so two machines' Firefox stay distinguishable. The catalog has to be resident for that to work. A channel opened when Cmd-K opens answers a second late, which is a second after the viewer typed the name of something the list did not yet know. So the catalog store holds one supervisor channel per connected server for the life of the page, armed like the roots watch and re-armed on the connection generation, since a channel does not survive a re-establish. The Manage panel now shares that channel rather than opening a second one. Two mirrors of one supervisor also meant two icon caches filling with the same artwork. Rows carry their icons, fetched as they scroll into view — the same lookahead the Manage panel uses, now shared, along with the two things about it that are easy to get wrong: the observer must be rooted at the scrolling element, because rootMargin does not reach past a clipping ancestor, and rows must register on mount, because a child's ref runs before its parent's. Measured on a 212-entry catalog: Cmd-K opens in 72 ms with every application listed, and starting one from it reaches the supervisor as `enabled no, phase starting`. Co-Authored-By: Claude <noreply@anthropic.com>
`CHANNEL_WATCH` added a `ChannelMessage::Names`, which left the systemd extension's match non-exhaustive: the whole extensions workspace stopped compiling, so `bin/extensions` produced nothing and no server could be given a new module. Both `Names` and `Opened` answer something a client asked, and this extension only ever serves channels — named rather than wildcarded, so the next server-side message added is a compile error here too, which is how this one was found. The panel's watcher subscription was registered with `onCleanup` inside a `then`. That runs with no reactive owner, so the cleanup was never called — Solid says exactly that on the console — and every open of the panel left a subscription behind for the life of the page. Hoisted into the effect's own cleanup, as the applications panel now does. Also the two markdown files `bin/lint` reformats on sight, so a run of it stops reporting a dirty tree. Co-Authored-By: Claude <noreply@anthropic.com>
Scrolling the applications list left a gap: a run of two dozen rows with no artwork, ending as abruptly as it began, and stable across scrolls and redraws. Measured against a real catalog, 212 entries came back 181 with artwork and 31 without — 28 of them consecutive, Portal Reloaded through Spelunky 2. The icon cache is dropped wholesale past MAX_CACHED_ICON_BYTES, and at MAX_ICON_BYTES per file that limit lands *part way through one batch*: a request for 48 Steam icons is more than the whole budget. `resolve_icons` filled the cache and then read its answers back out of it, so everything the batch had resolved before the drop came back "no artwork" — which the panel records as final and never asks about again. Hence a gap exactly one cache-worth long, and a perfectly good icon on the row after it. So a batch now holds its own resolutions and answers from those. The cache stays a cache: losing it costs a re-read, not a wrong answer. Two related distinctions come with it — a name whose lookup never ran is left unanswered rather than reported empty, because "ask me later" and "there is nothing to draw" are different things to a panel that treats the second as final; and the budget now holds one whole request, so a single scroll cannot clear the cache it is still filling. The same catalog now answers 209 of 212. The remaining three are 600 KB source PNGs — Steam writes full-size artwork into every size bucket, including 16x16 — which the read cap skips on purpose. Co-Authored-By: Claude <noreply@anthropic.com>
"Live" said only that the ordinary thing was happening: a journal pane tails by definition, so the badge was there for every reader of every pane and told none of them anything. The news is when it stops — which the same span already says, and now says alone. Nothing else moves. `data-journal-live` stays on the span, because it is how a test tells a tailing pane from a stopped one now that the pane itself is quiet, and the two stopped states keep their wording. `systemd.logsLive` has no callers left; it is still in i18n.ts because that file has other edits in flight. Co-Authored-By: Claude <noreply@anthropic.com>
`start` set the phase and nothing else. `attempt_due` then refused to launch anything that was not `enabled`, so the application sat in `Phase::Idle` — which every panel renders as "starting" — for the rest of the session, and no process was ever spawned. The verb has never worked on an application that was not already adopted; it only ever restarted an enabled one that had stopped. Starting Legcord from Cmd-K, or from `@session start`, did exactly nothing and said it was starting. The two verbs differ in one bit, and there was only one bit. `enabled` answers "should this run in every session from now on" and is persisted; `transient` answers "run it once, now" and lasts until the process exits. The supervisor launches either. Cleared by the exit, so a one-off run is not retried the way an enabled one is — trying an application is not asking for it to be kept alive — and by `stop`, or the next reconcile would restart what was just called off. The two are independent: a transient start on something already enabled leaves its restart loop alone. Verified on a scratch server: `@session start legcord` and the same from Cmd-K both reach `phase running` with a stamped socket and one window. Firefox still exits 255 on this machine, but that is its own NSS mismatch — `firefox --version` fails the same way with no blit in the picture. Co-Authored-By: Claude <noreply@anthropic.com>
`systemd.logsLive` lost its only caller when the pane stopped announcing that it was tailing. No other locale ever translated it, so this is the whole key. Co-Authored-By: Claude <noreply@anthropic.com>
Steam writes one full-size PNG into every size bucket, so those names resolve to five candidates and the read cap skips all five — a row with a letter tile and no bug behind it. Worth writing down next to the cap, along with why raising it is not the fix: base64 and one channel message put the ceiling at 640 KiB, which would cover Steam's files and nothing else, at nearly 900 KB on the wire apiece. Co-Authored-By: Claude <noreply@anthropic.com>
`PROCESS_STARTED` carries a status, and a non-OK one was dropped on the floor. But that message is the only word a refused spawn ever gets — there is no child, so no exit is coming either — and the application had already been moved to `Running` optimistically when the request was sent. It stayed there for the rest of the session, listed as running with nothing behind it, and `attempt_due` never reconsiders a phase it believes. An entry whose `Exec` names a binary that is not installed is the ordinary way to reach this, and it is not rare: a desktop file outlives the package that put it there. Counted as a failed run now, exactly as `reconcile` already treats a launch it could not even send — an enabled application backs off and retries, a one-off run stops. Verified both: a missing binary reports `stopped, last-exit -1` instead of `running`, and enabling it produces a growing backoff rather than a phantom. Also the line `bin/lint` reformats in SystemdLogs on sight. Co-Authored-By: Claude <noreply@anthropic.com>
Both examples asserted that the frame after `CREATE` was `CREATED`. It is not: the server pushes state whenever it has some, and a compositor with a cursor on it sends `SURFACE_CURSOR` unprompted, so both failed with `got 0x29` — five times out of five here, not intermittently. That is the whole test suite red at its last two steps, and everything after them unrun. Both already skipped unsolicited traffic while waiting for `READY`; they now do the same while waiting for a reply, bounded so a genuinely wrong opcode fails rather than hangs. Worth keeping as examples precisely because a reader copies this: a client that assumes replies arrive in isolation works until the machine it runs on has a window on it. Co-Authored-By: Claude <noreply@anthropic.com>
This test has been failing CI on main as `left: 5, right: 0`, and does not reproduce here — 23 runs, alone and alongside its module. Five is `EXT_EXIT_PROTOCOL_VIOLATION`, which the host reaches two ways: the logical connection closing before the guest returned, or an ABI misuse out of the wasmi host. The bare comparison discarded the one field that distinguishes them. The detail rides the assertion now, so the next occurrence names its own cause rather than needing a runner to reproduce it on. Co-Authored-By: Claude <noreply@anthropic.com>
Every running extension holds a connection of its own, so the clients list has always shown it — as "Client 7", indistinguishable from a browser tab and one click from a Kick that ends the attempt. The server knew better: the origin it builds at accept time carried the extension id all along and was dropped on the floor once the connection existed. Keep it on ClientState and report it when asked. A row now reads "session" with an extension tag, its attempt number beside the age, and a button that says Stop attempt, because that is what kicking it does. The task id is a random handle rather than an ordinal, so it stays in the row's tooltip with the coordinates that address the attempt elsewhere. Asking is opt-in through a trailing flags byte, answered with a new S2C_CLIENT_LIST2: both shipped parsers reject a catalog with bytes left over, so the wider entry has to be unreadable to them by construction rather than by convention. Its origin block is length-prefixed so a later kind needs no third opcode. Verified in all four directions — old client against new server unchanged, new client against old server degrading to "unknown". Co-Authored-By: Claude <noreply@anthropic.com>
The fs family had no one-shot read. `FS_FETCH` names a `sync_id`, so reading a handful of files meant syncing a directory nobody wanted watched — and the session supervisor took the other way out instead, spawning `/bin/sh` twice: once for every `.desktop` file at startup, once for every icon a panel asks for. The shell brought its own tax with it: quoting rules for every path it interpolated, `base64` to get bytes back through a text stream, and `wc -c` to enforce a size limit the protocol should have owned. So `FS_READ` (docs/design/fs-read.md): a list of paths, a per-file ceiling, and a status plus content per path, with no session and nothing watched. It grants no new authority — a client with FEATURE_FS could already sync a directory and fetch out of it — and it rides that same bit rather than a new one, both ends of this protocol being in this repo. `FS_READ_FIRST` answers the search-path question: the first of these paths that exists, in my order of preference, stepping over what is missing, unreadable or oversized. That is exactly an icon lookup, and it is what lets one message replace a shell loop over `[ -f ]`. The listing side needed two flags on `FS_INDEX`'s reserved byte to be usable for this: `DIRS_ONLY`, because a search path is the shape of a tree without its contents — an icon theme is fifty directories holding fifty thousand files — and `FOLLOW_LINKS`, because some trees are made of links. Independently of both, an entry that is a symlink is now classified by what it points at instead of being skipped: without that a Nix system `applications` directory indexes as empty, every `.desktop` in it being a link into the store, which also means file search has been blind to symlinked files all along. Measured against a real 212-entry catalog, before and after: the same 212 applications, the same 209 with artwork, the same three whose only copies are over the read cap. `/bin/sh` no longer appears in the extension at all, and neither does base64-through-stdout. One regression comes with it: filling all 212 icons went from 28.7s to 39.5s. Neither number is the read — a shell child answering 48 icons was already 135ms per icon — but a request per name is more messages than a child per batch, and the fix is to let one `FS_READ` carry a group of candidate lists rather than one. Co-Authored-By: Claude <noreply@anthropic.com>
…ts bytes
Two things were slow, and neither was the read.
`FS_READ` now carries groups: a group is one question, and with `FS_READ_FIRST`
each is answered by its own first readable path, one record per group in group
order. So the icon search asks once per batch instead of once per name. Measured
from inside the guest, the fs half of a 48-icon batch is now 63ms.
That left the guest itself, which is an interpreter, doing several passes over
every byte of artwork at about 325 KB/s:
- `push_json_string` pushed one `char` at a time. An icon is a 30 KB base64
data URL with nothing in it that needs escaping, so it now copies runs
between escapes — which for that string is one copy.
- `base64` also worked a `char` at a time, into a `String`. It writes bytes
into a sized buffer now. This was free before the migration off the shell,
where `base64(1)` did it natively.
- `flush_queued` cloned each 30 KB payload to send it and then memmoved the
rest of the queue. It takes and drains instead.
A 48-icon batch went from 8.8s to 4.7s. The whole 212-entry catalog is still
20-30s, because 6.5 MB of artwork through an interpreter is what it is: the same
range as the shell version, which pushed the same bytes.
The way out is to stop moving artwork through the guest at all — answer the panel
with the resolved *path* and let it read the bytes itself, now that a one-shot
read exists. That is a change to what the extension and the panel say to each
other, so it is not in here.
Co-Authored-By: Claude <noreply@anthropic.com>
An icon crossed the Wasm interpreter as a 30 KB base64 data URL inside a JSON string, and that cost more than everything else the session extension did: the bytes went through it at about 325 KB/s, so a screenful was seconds and the whole catalogue twenty of them. Nothing about that was the search — the reads themselves were 63ms. So the supervisor stops carrying artwork and starts naming it. `FS_READ` grew `FS_READ_NO_CONTENT`, which answers *which* path rather than what is in it — a stat per candidate — and the icon message carries `path` instead of `icon`. The panel then reads the file over the same one-shot read and hands the bytes to an `<img>` as an object URL, which is what it wanted all along: no base64, no JSON escaping, no copy per pass, and revocation on close so a panel that has been opened does not keep every icon it ever drew. Measured in a browser against a 212-entry catalogue: a screenful of 51 icons draws in 2.5s where the guest used to spend that on a dozen. The interpreter now touches no artwork at all — `base64` and `data_url` are gone from the extension. It also fixes the three rows that could not be fixed before. The 128 KiB cap existed because base64 grows a file by a third and the result had to fit one channel message; with the bytes going straight to the panel the ceiling is a megabyte, and Steam's habit of writing one 604 KB PNG into every size bucket — `16x16/apps/steam_icon_327030.png` included — is no longer a letter tile. Every icon that exists on this machine's disk now draws; what is left is entries whose `Icon=` names something nobody installed. Co-Authored-By: Claude <noreply@anthropic.com>
The Extensions tab had two tables over one identity: installed above, registry below. An extension present in both was named twice, and updating one looked exactly like installing a second — which is the one thing it must not look like, since replacing a definition keeps its identity and installing a second does not. So they are one list now, matched by name — the only handle the two sides share, the digest being precisely what differs when an update is available. A row that is both shows the digest it is pinned to next to the one on offer, and reads _Update_ or _Current_ accordingly. A transient `ext run` with no name matches nothing and keeps its own row rather than collapsing into another's. A registry was also a list of words and hashes, neither of which says what installing one would do. The manifest now carries each module's `package.description`, keyed by the bin target's name because that is what the published object is called — the crate is `blit-ext-systemd`, the module is `systemd.wasm`. Older registries have no descriptions, and a missing sentence is not a broken entry. Co-Authored-By: Claude <noreply@anthropic.com>
The banner above the unit table read "system: 412 units via signals, user: 88 units via signals" — a count that is already on the next line, and a source that is only worth a reader's attention when it is `poll`, which is the case where the table lags because there is no gdbus to subscribe with. So it says "Polling (no gdbus)" then, and nothing at all otherwise. Only `poll` counts: a scope is `unknown` until its first line lands, and announcing that as polling would flash the banner open on every panel. Co-Authored-By: Claude <noreply@anthropic.com>
…asked A long-lived shell has no command boundary of its own. Agents typed into one and then dumped scrollback, slept, or matched a regex against the whole grid — including text that was already on screen. OSC 133 (and OSC 633) already say where each command starts and ends. The server turns that into a bounded journal per PTY and addresses output by a monotonic sequence, so blit terminal journal / output / history --since can return one command, or only what is new, without pulling the backlog. wait --pattern now probes that cursor first. Shell integration stays opt-in: blit does not inject rc files. Hooks are in docs/shell-integration.md. A terminal whose shell emits nothing keeps an empty journal. Co-authored-by: Cursor <cursoragent@cursor.com>
|
|
🔗 Preview: https://blit-es06e4y68-indent.vercel.app |
Coverage
|
parse_mark uses `?` instead of a nested strip_prefix, the command-length test initializes the bound in the struct, and prettier gets the three files the previous fmt pass had left unformatted. Co-authored-by: Cursor <cursoragent@cursor.com>
AddAppSocket was copied from the Linux compositor with std::os::fd::OwnedFd, which Windows does not have. The non-Linux stub already has its own OwnedFd placeholder; use that so cargo build -p blit-cli compiles there. Co-authored-by: Cursor <cursoragent@cursor.com>
A taller viewport runs alacritty grow_lines, which shrinks history and adds from_history to cursor.line. saturating_sub treated that as zero, so rotated_lines stayed put and every captured sequence jumped. Follow the signed history delta instead: shrink still increments, grow decrements, and the cursor move cancels. Tests cover both directions; the RFC now has the Resize section the code already cited. Co-authored-by: Cursor <cursoragent@cursor.com>
1 task
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.
Why
A long-lived shell PTY has no command boundary. An agent that types into one today dumps scrollback, sleeps, or matches a regex against the whole grid — including text that was already on screen.
blit terminal wait --patterndocumented “lines produced after the wait began” and then re-scannedget_all_text()on every update.What
OSC 133 (FinalTerm) and OSC 633 (VS Code) already say where each command starts, where its output starts, and how it ended. The server turns that into a bounded journal per PTY and addresses output by a monotonic sequence (
rotated_lines + row), not a grid row that moves under the reader.FEATURE_TERM_JOURNAL(bit 28), opcodes0x50–0x53.BLIT_TERM_JOURNAL=0withholds the bit and refuses withPERMISSION.blit terminal journal,blit terminal output [--wait],history --since CURSOR. Defaults are bounded.--jsonon all three.wait --pattern: probes the sequence cursor first, then matches onlyTERM_SINCEtext. Falls back to the old grid scan against a server without the bit.docs/shell-integration.md. A terminal whose shell emits nothing keeps an empty journal at no cost.RFC:
docs/design/term-journal.md.Note on the branch
This branch currently sits on unpublished local
main(extensions,FS_READ, client origin, … — 33 commits ahead oforigin/main). The journal work is the tip commit. Rebase ontoorigin/mainhit conflicts in those files; happy to restack once those land, or to isolate the tip if you’d rather this PR not carry them.Test plan
cargo test -p blit-alacritty -p blit-remote -p blit-server -p blit-cli(699 passed)blit terminal start --cols 200, enable the OSC 133 hook,send 'echo hi\n', thenjournal/output --waithistory --since now, produce output,history --since <cursor>returns only the new textwait --patternagainst text already on screen does not return immediatelyBLIT_TERM_JOURNAL=0withholds the feature bitMade with Cursor