Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
e3b4e26
Probe the camera on a canvas, and size audio for the sink that plays it
pcarrier Aug 18, 2026
79e6929
Answer for a repository's branches and worktrees
pcarrier Aug 18, 2026
804b051
Stamp a window with the application it came from
pcarrier Aug 18, 2026
a64f3a9
Give extensions a home, a build, and a way to publish them
pcarrier Aug 18, 2026
852a6ca
Mirror systemd's units and journal into an extension
pcarrier Aug 18, 2026
c84ea7d
Let a session bring its own applications up, and keep them up
pcarrier Aug 18, 2026
66c9892
Give a remote panels for its extensions, units and applications
pcarrier Aug 18, 2026
d18d80b
Show branches and worktrees in the left dock
pcarrier Aug 18, 2026
4a2bef7
Reach the dev extension registry through the page's origin
pcarrier Aug 18, 2026
9bdb7ec
Tail the journal live, and read its history by scrolling
pcarrier Aug 18, 2026
86ca6fd
Call a remote's panel Manage, and assert the registry it can reach
pcarrier Aug 18, 2026
cbdccf6
Pass held modifiers to a surface that takes focus later
pcarrier Aug 18, 2026
85cafaa
Carry a held modifier into the click that focuses a surface
pcarrier Aug 18, 2026
890b776
Show every application's own icon in the Applications tab
pcarrier Aug 18, 2026
0100671
Show the whole catalog, not just what a search summons
pcarrier Aug 18, 2026
5ca9829
Keep the catalog's artwork up with the scroll
pcarrier Aug 18, 2026
0f49739
Watch which channel names a server serves, and name the tab Session
pcarrier Aug 18, 2026
0f31b1d
Start any installed application from Cmd-K
pcarrier Aug 18, 2026
246ce37
Build the extensions again, and stop leaking unit subscriptions
pcarrier Aug 18, 2026
52c2953
Answer an icon batch from what it resolved, not from a cache it emptied
pcarrier Aug 18, 2026
a7890f6
Stop telling a journal reader that the journal is live
pcarrier Aug 18, 2026
32f30d2
Actually start an application that was only ever started
pcarrier Aug 18, 2026
39c52e6
Retire the string a quiet journal pane no longer says
pcarrier Aug 18, 2026
783cd5d
Say why a games machine keeps three letter tiles
pcarrier Aug 18, 2026
f026944
Stop reporting a spawn that was refused as a running application
pcarrier Aug 18, 2026
18fb0e0
Wait for the reply in the fd-channel examples, not for the next frame
pcarrier Aug 18, 2026
c467921
Say why the guest exited when the upload test fails
pcarrier Aug 18, 2026
85a8d69
Say which connections are extensions in the clients list
pcarrier Aug 18, 2026
0ca3c59
Read files with the fs family instead of a shell
pcarrier Aug 18, 2026
05a239f
Ask for a screenful of icons in one message, and stop crawling over i…
pcarrier Aug 18, 2026
31d3ffe
Let the panel read the artwork it draws
pcarrier Aug 18, 2026
9ad3946
Name an extension once, and say what it is for
pcarrier Aug 18, 2026
9f0133c
Mention the unit table's source only when it is the slow one
pcarrier Aug 18, 2026
c9c6e66
Record each shell command, and wait on output that arrived after you …
pcarrier Aug 18, 2026
cd2f4f0
Satisfy clippy and prettier on the journal branch
pcarrier Aug 18, 2026
ca68ac4
Name the stub app-socket fd the same type the rest of the stub uses
pcarrier Aug 18, 2026
f06fb9a
Keep sequence identity when a resize pulls history back into view
pcarrier Aug 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 44 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,25 @@ jobs:
with:
upload: true

# Wasm is architecture-independent, so one leg builds the extension modules
# every platform's release points at.
build-extensions:
needs: [verify-tag]
runs-on: blacksmith-8vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v7
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- run: ./bin/extensions
- uses: actions/upload-artifact@v7
with:
name: extensions
path: |
extensions/dist/*.wasm
extensions/dist/manifest.json

release:
needs: [build-packages, build-windows]
needs: [build-packages, build-windows, build-extensions]
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/download-artifact@v8
Expand All @@ -61,19 +78,30 @@ jobs:
name: windows-x86_64
path: windows

- uses: actions/download-artifact@v8
with:
name: extensions
path: extensions

- name: List artifacts
run: |
ls -lh debs/
ls -lh tarballs/
ls -lh windows/
ls -lh extensions/

# Release assets are the durable home for a module: Pages only ever
# serves the current release, so a `#digest` pin outlives its version
# here and nowhere else.
- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
files: |
debs/*.deb
tarballs/*.tar.gz
windows/*.zip
extensions/*.wasm
extensions/manifest.json
generate_release_notes: true

publish-crates:
Expand Down Expand Up @@ -144,7 +172,7 @@ jobs:
client-payload: '{"version": "${{ github.ref_name }}"}'

apt-repo:
needs: [build-packages, build-windows]
needs: [build-packages, build-windows, build-extensions]
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v7
Expand All @@ -166,6 +194,11 @@ jobs:
name: windows-x86_64
path: windows

- uses: actions/download-artifact@v8
with:
name: extensions
path: extensions

- name: Import GPG key
run: echo "$GPG_PRIVATE_KEY" | gpg --batch --import
env:
Expand All @@ -189,6 +222,15 @@ jobs:
cp "$f" repo/bin/
done

# Extension modules, for `blit ext run https://install.blit.sh/ext/…`.
# Like repo/bin, this is the current release only: Pages publishes
# the tree wholesale, so an older module's URL stops resolving. The
# manifest carries each module's BLAKE3, which is what a `#digest`
# pin needs and what the GitHub Release keeps forever.
mkdir -p repo/ext
cp extensions/*.wasm repo/ext/
cp extensions/manifest.json repo/ext/manifest.json

# Write version to /latest
echo "${GITHUB_REF_NAME#v}" > repo/latest

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ coverage/
dist
node_modules/
node_modules
/extensions/target/
23 changes: 16 additions & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,21 +147,29 @@ There is no `rustfmt.toml` or `.clippy.toml` — default rustfmt, prettier, and
| `gateway` | Runs `blit gateway` with WebSocket + WebTransport (pass=`dev`) | `127.0.0.1:10001` |
| `ui` | Vite dev server for `js/ui/` | `127.0.0.1:10000` |
| `website` | Astro dev server for `js/website/` | `127.0.0.1:10002` |
| `extensions` | Builds `extensions/dist`, then serves it as a CORS extension registry | `127.0.0.1:10003` |

### Running multiple dev stacks

Every port and socket path is derived from `DEV_INSTANCE` (default `0`). Each instance gets a block of ports at `10000 + (N * 5)`:
Every port and socket path is derived from `DEV_INSTANCE` (default `0`). Each instance gets a block of ports at `10000 + (N * 10)`:

| Instance | UI | Gateway | Website |
| -------- | ----- | ------- | ------- |
| 0 | 10000 | 10001 | 10002 |
| 1 | 10005 | 10006 | 10007 |
| 2 | 10010 | 10011 | 10012 |
| Instance | UI | Gateway | Website | Extensions |
| -------- | ----- | ------- | ------- | ---------- |
| 0 | 10000 | 10001 | 10002 | 10003 |
| 1 | 10010 | 10011 | 10012 | 10013 |
| 2 | 10020 | 10021 | 10022 | 10023 |

```bash
DEV_INSTANCE=1 ./bin/dev # second stack on 10005-10007
DEV_INSTANCE=1 ./bin/dev # second stack on 10010-10013
```

The UI dev server proxies `/ext` to its own instance's registry, so the
Extensions tab of a remote offers the modules _this_ stack built rather than
the published ones. It goes through the page's origin rather than straight to
`127.0.0.1:10003` so that it also works when the dev UI is reached through a
tunnel or reverse proxy, where there is no port to derive from and the registry
port is not published.

`bin/dev` prints the concrete addresses on startup. `DEV_INSTANCE` is intentionally unprefixed: blit strips most `BLIT_*` variables from child terminals, but passes everything else through. This means `DEV_INSTANCE` propagates into nested shells so you always know which instance you're inside and can pick a different one. You can also override individual values:

| Variable | Default (instance 0) | Description |
Expand All @@ -172,6 +180,7 @@ DEV_INSTANCE=1 ./bin/dev # second stack on 10005-10007
| `BLIT_DEV_GW_PORT` | `10001` | Gateway TCP + WebTransport UDP port |
| `BLIT_DEV_WT_ADDR` | `:10001` | Browser-facing WebTransport `hostname:port` or `:port` |
| `BLIT_DEV_SITE_PORT` | `10002` | Astro website dev-server port |
| `BLIT_DEV_EXT_PORT` | `10003` | Extension registry (`extensions/dist`) port |

## Project structure

Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ members = [
# A path dependency living under the workspace root would otherwise become an
# implicit member, so `--workspace` clippy/test/fmt runs would lint, test, and
# reformat third-party code we only consume.
exclude = ["vendor/cros-codecs", "vendor/wayland-backend"]
exclude = ["vendor/cros-codecs", "vendor/wayland-backend", "extensions"]

[workspace.package]
edition = "2024"
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ Control terminals programmatically:
blit terminal start htop # start a terminal, print its ID
blit terminal show 1 # dump current terminal text
blit terminal send 1 q # send keystrokes
blit terminal journal 1 # commands the shell has run (needs OSC 133)
blit terminal output 1 --wait 60 # that command's output
```

Inspect and disconnect other clients attached to the same server:
Expand Down
7 changes: 6 additions & 1 deletion SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ description: >
language server for definitions, references, hover, completions, signature
help, symbols, diagnostics, or a rename plan; read or write the server's
key/value store; or run and interact with GUI applications. Covers
starting PTYs, sending keystrokes, reading output, checking exit status,
starting PTYs, sending keystrokes, reading output, listing the commands a
shell has run, waiting on a command's output, checking exit status,
managing terminal lifecycle, attaching a local terminal to a remote one,
searching file contents across a tree, and driving graphical windows through the experimental headless Wayland
compositor (listing surfaces, capturing screenshots, clicking, typing,
Expand All @@ -32,6 +33,10 @@ filesystem, git, and language-server commands. Beyond terminals it can:
backed by real language servers running next to the code.
- **Key/value store** — `blit kv get|put|rm|ls`, prefix-watchable, with
compare-and-swap writes. Handy as host-local scratch space for scripts.
- **Command journal** — `blit terminal journal|output` and
`history --since`, given a shell that emits OSC 133 (see
`docs/shell-integration.md`). `wait --pattern` matches only output
produced after the wait began.

## Install

Expand Down
14 changes: 11 additions & 3 deletions bin/dev
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ cd "$(dirname "$0")/.."
# Set DEV_INSTANCE to run multiple dev stacks side-by-side. Each instance
# gets a block of ports starting at 10000 + (N * 10):
#
# Instance 0 (default): 10000 10001 10002
# Instance 1: 10010 10011 10012
# Instance 2: 10020 10021 10022
# Instance 0 (default): 10000 10001 10002 10003
# Instance 1: 10010 10011 10012 10013
# Instance 2: 10020 10021 10022 10023
# ...
#
# The blit server Unix socket is /tmp/blit-dev.sock for instance 0,
Expand All @@ -32,16 +32,24 @@ N="${DEV_INSTANCE:-0}"

if [ "$N" -eq 0 ]; then
export BLIT_DEV_SOCK="${BLIT_DEV_SOCK:-/tmp/blit-dev.sock}"
export BLIT_DEV_EXT_DB="${BLIT_DEV_EXT_DB:-/tmp/blit-dev-ext/extensions.redb}"
PC_SOCK="/tmp/blit-dev-pc.sock"
else
export BLIT_DEV_SOCK="${BLIT_DEV_SOCK:-/tmp/blit-dev-${N}.sock}"
export BLIT_DEV_EXT_DB="${BLIT_DEV_EXT_DB:-/tmp/blit-dev-${N}-ext/extensions.redb}"
PC_SOCK="/tmp/blit-dev-pc-${N}.sock"
fi

BASE=$(( 10000 + N * 10 ))
export BLIT_DEV_UI_PORT="${BLIT_DEV_UI_PORT:-$(( BASE ))}"
export BLIT_DEV_GW_PORT="${BLIT_DEV_GW_PORT:-$(( BASE + 1 ))}"
export BLIT_DEV_SITE_PORT="${BLIT_DEV_SITE_PORT:-$(( BASE + 2 ))}"
# The stack's own extension registry: extensions/dist served with CORS, which
# is what the UI's Extensions panel installs from in dev. The vite dev server
# is told this port and proxies /ext to it, so a second instance's page reaches
# that instance's modules and not the first one's — and a page reached through
# a tunnel reaches them at all, which a bare loopback port cannot offer.
export BLIT_DEV_EXT_PORT="${BLIT_DEV_EXT_PORT:-$(( BASE + 3 ))}"
# Browser-facing WebTransport authority. A leading `:port` keeps the browser
# page hostname; `hostname:port` overrides both.
export BLIT_DEV_WT_ADDR="${BLIT_DEV_WT_ADDR:-:${BLIT_DEV_GW_PORT}}"
Expand Down
65 changes: 65 additions & 0 deletions bin/ext-registry
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env node
/**
* Serve `extensions/dist` as an extension registry, the way
* https://install.blit.sh/ext serves one: `manifest.json` plus the modules it
* names, with CORS open so a page on the vite dev port can install from it.
*
* Node rather than a static-file package because the dev shell has node and
* nothing else guaranteed — no python3 — and this is thirty lines.
*
* bin/ext-registry [port] [directory]
*/

import { createReadStream, statSync } from "node:fs";
import { createServer } from "node:http";
import { extname, join, normalize, resolve } from "node:path";

const port = Number(process.argv[2] ?? process.env.BLIT_DEV_EXT_PORT ?? 10003);
const root = resolve(process.argv[3] ?? "extensions/dist");

const TYPES = {
".wasm": "application/wasm",
".json": "application/json",
".br": "application/octet-stream",
};

createServer((request, response) => {
const cors = {
"access-control-allow-origin": "*",
"access-control-allow-headers": "*",
};
if (request.method === "OPTIONS") {
response.writeHead(204, cors).end();
return;
}
// Everything under the root, and nothing above it: a leading `..` in the
// request path must not be able to name the rest of the checkout.
const requested = decodeURIComponent(
new URL(request.url ?? "/", "http://localhost").pathname,
);
const path = join(root, normalize(requested).replace(/^(\.\.[/\\])+/, ""));
if (!path.startsWith(root)) {
response.writeHead(403, cors).end("forbidden\n");
return;
}
let size;
try {
const stat = statSync(path);
if (!stat.isFile()) throw new Error("not a file");
size = stat.size;
} catch {
response.writeHead(404, cors).end("not found\n");
return;
}
response.writeHead(200, {
...cors,
"content-type": TYPES[extname(path)] ?? "application/octet-stream",
"content-length": String(size),
// The digest is the identity, so a stale copy is a wrong install rather
// than an old one. In a dev stack the modules are rebuilt constantly.
"cache-control": "no-store",
});
createReadStream(path).pipe(response);
}).listen(port, () => {
console.log(`extension registry: http://127.0.0.1:${port} -> ${root}`);
});
4 changes: 4 additions & 0 deletions bin/extensions
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
exec nix run .#extensions -- "$@"
Loading
Loading