deps: follow felis to v0.1.0 #3

Merged
natsukium merged 8 commits from follow-felis-105b0899 into main 2026-09-26 20:57:56 +09:00
Owner

Follows felis from 32775067 to the v0.1.0 release (https://github.com/felis-terminal/felis, tag v0.1.0), across the wire-baseline reset. The branch keeps its earlier commits (the reset follow to 105b0899) and adds the move to the release on top.

Closes #8
Closes #1

Pins

  • Cargo: every felis crate is git = "https://github.com/felis-terminal/felis", tag = "v0.1.0" (felis is not on crates.io). nix/web.nix outputHashes refreshed.
  • Flake (demo only): felis → github:felis-terminal/felis/v0.1.0; felis-web-gateway → ?ref=follow-felis-v0.1.0, the branch of natsukium/felis-web-gateway#4. Re-lock that input to the gateway's main once #4 merges.
  • The old blocker (felis-client-core not building with default-features = false) is fixed upstream at v0.1.0; every gate runs unpatched.

What changed (v0.1.0 over the earlier commits here)

  • felis-json v1 everywhere (#8). Frames arrive in felis's own envelope {"felis_json":1,"kind":"grid"|"image"|"conn"|"session","msg":…} (broker: sid beside it). applyJson/applyImageJson read msg through felis's json_v1 DTOs (which repack rows into the row codec; transcode is gone upstream). The TS router uses lowercase kinds and the type tag; both transports refuse an unknown felis_json rather than paint a screen missing state.
  • Input is the v1 input payload: key_bytes, paste, resize, viewport, focus_change, jump_prompt, mouse (modifier bit set, snake_case buttons/actions).
  • Keys are encoded by the daemon. felis moved key encoding daemon-side, so the wasm encodeKey is replaced by keyInput, which builds the wire Key event (key / text / mods / press-repeat-release / location) and refuses text past the per-keystroke cap. Space is sent as the named key (Ctrl+Space → NUL). The mode getters that only fed the old encoder are removed.
  • Roster rows are SessionInfoJson (id, dims, sequence, lowercase urgency); SessionEntry gains sequence and the mobile demo restores tabs in creation order.
  • Fixtures (#1). All 12 web/public/*.fcast are rewritten as .fcast v3 / felis-json v1 by a one-to-one variant mapping (the old frames hold everything v3 needs; several could not be re-recorded identically). gen-glyphs-fcast.py now emits v3, and regenerating from it matches the converted file exactly. New tests/fixtures.rs replays every fixture frame by frame, so a future format change breaks cargo test rather than only the gallery.
  • Bench generator emits v1 shapes; README/docs/mobile.md/Cargo comments updated.

Verification

  • cargo fmt --all --check, cargo clippy --all-targets --target wasm32-unknown-unknown -D warnings, cargo test --target wasm32-unknown-unknown (25 apply + 10 key_input + 2 fixtures), cargo deny check, wasm build + wasm-bindgen + tsgo -p . --noEmit, nix build .#web: all pass locally.
  • Live, against a v0.1.0 felis-daemon (isolated socket) through the gateway from natsukium/felis-web-gateway#4, driving mobile.html in headless Chromium: roster → tab attach (wire minor 0), fit resize, typed commands, Enter, Ctrl+C interrupting sleep, ArrowUp history recall, and the shortcut bar's git status⏎ all reached the shell and rendered.
  • index.html gallery: every converted recording replays with no console errors (btm, glyphs, kitty image, OSC 66 sizing, OSC 8 links, OSC 10/11/12 theme checked).
  • The real-daemon row in tests/apply.rs was re-captured from the v0.1.0 daemon through the gateway.

Still open

Follows felis from `32775067` to the **v0.1.0** release (https://github.com/felis-terminal/felis, tag `v0.1.0`), across the wire-baseline reset. The branch keeps its earlier commits (the reset follow to `105b0899`) and adds the move to the release on top. Closes #8 Closes #1 ## Pins - Cargo: every felis crate is `git = "https://github.com/felis-terminal/felis", tag = "v0.1.0"` (felis is not on crates.io). `nix/web.nix` `outputHashes` refreshed. - Flake (demo only): `felis` → `github:felis-terminal/felis/v0.1.0`; `felis-web-gateway` → `?ref=follow-felis-v0.1.0`, the branch of natsukium/felis-web-gateway#4. **Re-lock that input to the gateway's `main` once #4 merges.** - The old blocker (felis-client-core not building with `default-features = false`) is fixed upstream at v0.1.0; every gate runs unpatched. ## What changed (v0.1.0 over the earlier commits here) - **felis-json v1 everywhere (#8).** Frames arrive in felis's own envelope `{"felis_json":1,"kind":"grid"|"image"|"conn"|"session","msg":…}` (broker: `sid` beside it). `applyJson`/`applyImageJson` read `msg` through felis's `json_v1` DTOs (which repack rows into the row codec; `transcode` is gone upstream). The TS router uses lowercase kinds and the `type` tag; both transports refuse an unknown `felis_json` rather than paint a screen missing state. - **Input** is the v1 `input` payload: `key_bytes`, `paste`, `resize`, `viewport`, `focus_change`, `jump_prompt`, `mouse` (modifier bit set, snake_case buttons/actions). - **Keys are encoded by the daemon.** felis moved key encoding daemon-side, so the wasm `encodeKey` is replaced by `keyInput`, which builds the wire `Key` event (key / text / mods / press-repeat-release / location) and refuses text past the per-keystroke cap. Space is sent as the named key (Ctrl+Space → NUL). The mode getters that only fed the old encoder are removed. - **Roster rows** are `SessionInfoJson` (`id`, `dims`, `sequence`, lowercase urgency); `SessionEntry` gains `sequence` and the mobile demo restores tabs in creation order. - **Fixtures (#1).** All 12 `web/public/*.fcast` are rewritten as `.fcast` v3 / felis-json v1 by a one-to-one variant mapping (the old frames hold everything v3 needs; several could not be re-recorded identically). `gen-glyphs-fcast.py` now emits v3, and regenerating from it matches the converted file exactly. New `tests/fixtures.rs` replays every fixture frame by frame, so a future format change breaks `cargo test` rather than only the gallery. - Bench generator emits v1 shapes; README/`docs/mobile.md`/Cargo comments updated. ## Verification - `cargo fmt --all --check`, `cargo clippy --all-targets --target wasm32-unknown-unknown -D warnings`, `cargo test --target wasm32-unknown-unknown` (25 apply + 10 key_input + 2 fixtures), `cargo deny check`, wasm build + `wasm-bindgen` + `tsgo -p . --noEmit`, `nix build .#web`: all pass locally. - Live, against a v0.1.0 `felis-daemon` (isolated socket) through the gateway from natsukium/felis-web-gateway#4, driving `mobile.html` in headless Chromium: roster → tab attach (`wire minor 0`), fit resize, typed commands, Enter, Ctrl+C interrupting `sleep`, ArrowUp history recall, and the shortcut bar's `git status⏎` all reached the shell and rendered. - `index.html` gallery: every converted recording replays with no console errors (btm, glyphs, kitty image, OSC 66 sizing, OSC 8 links, OSC 10/11/12 theme checked). - The real-daemon row in `tests/apply.rs` was re-captured from the v0.1.0 daemon through the gateway. ## Still open - #2: search / scrollback export remain unreachable from a browser (gateway issue natsukium/felis-web-gateway#1 is the prerequisite).
felis reset its wire baseline to 1.0 on 2026-09-05, so the pin this
crate carried (32775067, 2026-07-16) reads a protocol that no longer
exists. Bump every felis-* rev in lockstep and adapt the wasm core to
what the reset left:

  - `ShadowGrid` is `ShadowScreen`, built from `(rows, cols)` alone. The
    per-connection `Encoding` is gone with the proto3-JSON leg, so the
    shadow always decodes the packed row codec; `applyJson` recodes a
    frame's structural-JSON rows through `felis_grid::transcode` rather
    than teaching the view a second row encoding, because the codec's
    version tag gates a future layout and only one side may know it.
  - `RowDelta` carries `rows: [(row, payload)]` and absorbed
    `RowDeltaBatch`; `GridSize` is `Size { dims }`; `RehydrateBegin` is
    payload-free.
  - `encode_row_with` / `decode_row_with` split into `encode_row` /
    `decode_row` (packed) and their `_json` twins, and take a
    `RowEncode` with an explicit `pad_to`. The replay scrollback ring
    stores the packed form now: nothing outside it reads those bytes.
  - `key_encode::encode` takes `KittyKbdFlags` and `ModifyOtherKeys`
    instead of raw levels; the JS mirror keeps its numeric wire shape
    and converts at the boundary.
  - the shadow no longer mirrors OSC 10/11/12, so the view keeps the
    three theme channels itself for `themeJson`.
  - image transfers are canonical: `Header` carries an `ImageTarget`,
    and `total_bytes` / a chunk's `offset` are gone. `ImageShadow::apply`
    is fallible now, and a malformed sequence surfaces as a JS error.
  - `virtual_placement(id)` became the `virtual_placements()` slice.

`GridMsg::ScrollbackRow` is gone with the family split: a capture reply
is `RegionMsg::Row`, which carries text the daemon already resolved. The
`captureRow` export decoded that payload and has no source left, so it
goes; its real-daemon row fixture moves onto `applyJson`, where the same
bytes now arrive.
web: follow the wire reset through the component and its docs
Some checks failed
ci / fmt / clippy / test / deny / types / bundle (pull_request) Failing after 31s
edfb8dfaeb
The reset took the capability bitmap with it: a connection now states one
`ConnectionMode` and negotiates a protocol minor, and there is nothing
per-feature left to intersect. So `Attached.caps` becomes `Attached.minor`
and `caps()` becomes `protocolMinor()` — the number a browser feature
needing a wire addition gates on. Both transports already carry it: the
gateway's attach reply names `minor`, and a `.fcast` v2 header names
`proto_minor`.

Search and scrollback export left `InputMsg` in the same reset. They are
`Search` / `Region` requests now, correlated families whose request ids
the connection driver allocates, and the gateway's broker routes neither
direction of either — so a browser has no way to ask. Keeping the old
`sendInput` calls would be worse than useless: an undecodable frame no
longer gets skipped, it ends the connection, so pressing search would
have cost the user their session. `search`, `cancelSearch` and
`captureText` for anything but `"visible"` refuse locally instead, and
the reply plumbing they fed goes with them. `captureText("visible")`
still answers from the shadow screen, this side of the wire.

The bench workload generator emits `RowDelta`'s new `(row, payload)`
shape, and `docs/mobile.md` gains a preface saying which of its wire
facts the reset invalidated — the same treatment its paths got when the
gateway split out, and for the same reason: it records a decision
sequence, so rewriting it would make it claim a wire that did not exist
while the work was done.

`nix/web.nix`'s vendoring hash follows the rev.
felis 105b0899's wire reset moved InputMsg::Resize's four axes behind a
`dims` field, wrapped GridMsg::ClipboardSet's body in `write`, and
merged Bell/Notify into GridMsg::Attention. The follow commit missed all
three, and each fails silently on a live session:

- the gateway drops the flat Resize as `missing field dims` with only an
  eprintln, so `resize="fit"` is inert and the daemon never learns the
  cell pixel metrics a Kitty-graphics producer reads out of
  ws_xpixel/ws_ypixel;
- every OSC 52 reaches onClipboard as `{text: "", clipboard: false}` —
  a lie rather than a no-op — and the clipboardMode="write" shortcut,
  gated on that flag, never fires;
- routeGrid's default arm swallows Attention, so onNotify never fires.

Attention carries both a bell and a desktop notification, and felis
routes the two into one client-side handler, so onNotify fires for
either; its doc comment claimed OSC 9/99/777 only and still named the
NOTIFY cap the reset removed.

Refs natsukium/felis-web-component#4, #5, #6.

Assisted-by: Claude Code
fix(web): carry the post-exit grace onto broker roster rows
Some checks failed
ci / fmt / clippy / test / deny / types / bundle (pull_request) Failing after 14s
02c67645be
felis's SessionInfo grew `exited` and `last_exit_code` and the gateway
puts both on every roster row, but SessionEntry stopped at `foreground`.
A restored tab therefore attaches to a session whose shell is gone and
presents it as an ordinary one: the final screen paints, keystrokes go
nowhere, and the tab dies whenever the grace expires. mobile.html marks
such a tab with its own glyph rather than reusing the dead-stream ⚠,
which promises a re-attach an exited shell cannot honour.

idleSeconds' doc comment was stale in the same type: felis returns None
while attached, so `0` now means "detached for under a second" and not
"in use" — and that comment is what an app reads when deciding what 0
means. toEntry already mapped the value correctly.

Refs natsukium/felis-web-component#7.

Assisted-by: Claude Code
Author
Owner

Pushed two commits fixing the four cross-check findings (#4-#7); each shape was re-verified against felis main's serde definitions before editing.

d2c915d — fix(web): follow felis's wire reset for resize, clipboard and attention (#4, #5, #6)

  • sendResize now emits {"Resize":{"dims":{rows,cols,pixel_w,pixel_h}}}, matching InputMsg::Resize { dims: RequestedDims }.
  • routeGrid's ClipboardSet arm unwraps write before reading selection/data, matching ClipboardSet { write: ClipboardWrite } (selection stays the raw u8 bits — ClipboardSelection's hand-written Serialize is unchanged).
  • The Notify arm became Attention. AttentionSource is not passed through: felis routes Bell and Notification into one client-side handler, so onNotify fires for either. Its doc comment said "OSC 9/99/777" only and still named the NOTIFY cap the reset removed; both are corrected, as is the README.md bullet and the stale InputMsg::Resize { rows, cols, … } row in docs/mobile.md.

02c6764 — fix(web): carry the post-exit grace onto broker roster rows (#7)

  • SessionEntry gains exited: boolean and lastExitCode: number | null; toEntry reads exited / last_exit_code.
  • web/mobile.html marks a restored exited tab with ☠ and an exit-code tooltip, kept distinct from the dead-stream ⚠ — that one promises a re-attach, which an exited shell cannot honour.
  • idleSeconds' doc comment corrected: null while attached, so 0 is "detached for under a second".

Gates

  • nix develop -c tsgo -p . --noEmit — pass (against a freshly built web/pkg).
  • nix develop -c cargo fmt --all --check — pass.
  • nix develop -c cargo deny check — pass.
  • cargo build/test --target wasm32-unknown-unknown — still red for the reason tracked in natsukium/felis#195; untouched here.

No test was added: all four fixes live in the TypeScript layer, and the repo has no TS test harness (tests/ is wasm-bindgen Rust, bench/ is a render harness, and no package script runs a JS runner). tests/apply.rs cannot see any of these — it decodes through the typed enum, which is exactly why the follow commit passed its gates.

Pushed two commits fixing the four cross-check findings (#4-#7); each shape was re-verified against felis `main`'s serde definitions before editing. **d2c915d — `fix(web): follow felis's wire reset for resize, clipboard and attention`** (#4, #5, #6) - `sendResize` now emits `{"Resize":{"dims":{rows,cols,pixel_w,pixel_h}}}`, matching `InputMsg::Resize { dims: RequestedDims }`. - `routeGrid`'s `ClipboardSet` arm unwraps `write` before reading `selection`/`data`, matching `ClipboardSet { write: ClipboardWrite }` (`selection` stays the raw `u8` bits — `ClipboardSelection`'s hand-written `Serialize` is unchanged). - The `Notify` arm became `Attention`. `AttentionSource` is not passed through: felis routes `Bell` and `Notification` into one client-side handler, so `onNotify` fires for either. Its doc comment said "OSC 9/99/777" only and still named the `NOTIFY` cap the reset removed; both are corrected, as is the `README.md` bullet and the stale `InputMsg::Resize { rows, cols, … }` row in `docs/mobile.md`. **02c6764 — `fix(web): carry the post-exit grace onto broker roster rows`** (#7) - `SessionEntry` gains `exited: boolean` and `lastExitCode: number | null`; `toEntry` reads `exited` / `last_exit_code`. - `web/mobile.html` marks a restored exited tab with `☠` and an exit-code tooltip, kept distinct from the dead-stream `⚠` — that one promises a re-attach, which an exited shell cannot honour. - `idleSeconds`' doc comment corrected: `null` while attached, so `0` is "detached for under a second". **Gates** - `nix develop -c tsgo -p . --noEmit` — pass (against a freshly built `web/pkg`). - `nix develop -c cargo fmt --all --check` — pass. - `nix develop -c cargo deny check` — pass. - `cargo build/test --target wasm32-unknown-unknown` — still red for the reason tracked in natsukium/felis#195; untouched here. No test was added: all four fixes live in the TypeScript layer, and the repo has no TS test harness (`tests/` is `wasm-bindgen` Rust, `bench/` is a render harness, and no package script runs a JS runner). `tests/apply.rs` cannot see any of these — it decodes through the typed enum, which is exactly why the follow commit passed its gates.
felis v0.1.0 is the first public release, published at
github.com/felis-terminal/felis; the crates are not on crates.io, so
every felis dependency now pins that tag, and the demo's flake inputs
follow: felis at the same tag, and felis-web-gateway at the branch that
moves the relay to it (to be re-locked to its main once that lands).
The gateway and this component decode the same frames, so the two move
together.

What the release changed under the component:

- The gateway re-exposes frames as felis's own felis-json v1 envelope
  (`{"felis_json":1,"kind":"grid",…}`), not the derived serde of the
  Rust enums. `applyJson` / `applyImageJson` read the payload through
  felis's `json_v1` DTOs, which also repack each row into the row codec
  the shadow screen decodes, so the component forks no shape; the
  `transcode` recode it used is gone upstream. The TS router reads the
  lowercase kind tokens and the `type` tag, and both transports refuse a
  frame in a felis-json version they do not read instead of painting a
  screen that is missing state.
- Input goes back as the v1 `input` payload (`{"type":"key_bytes",…}`,
  `{"type":"mouse","event":{…}}` with the modifier bit set, …).
- Key encoding moved into the daemon, which owns the keyboard modes, so
  the wasm `encodeKey` became `keyInput`: it names the key, modifiers,
  press/repeat/release and location as the wire's `Key` event and
  refuses text past the per-keystroke cap the daemon would end the
  connection over. Space goes out as the named key, the only form in
  which Ctrl+Space encodes. The mode getters that only fed the old
  encoder are dropped.
- Roster rows are `SessionInfoJson` (`id`, `dims`, `sequence`); the
  mobile demo restores tabs in creation order from `sequence`.
- `ThemeColor` carries a set/reset action, and a row naming a cluster
  that was never installed is now refused by the shadow screen rather
  than painted as U+FFFD, so that test asserts the refusal.

The bench workload generator emits the v1 shapes too. The captured
real-daemon row in the apply tests was re-taken from a v0.1.0 daemon
through the gateway.

Search and scrollback export stay unreachable from a browser: the
gateway still routes neither correlated family.

Assisted-by: Claude Code
fix(web): rewrite the demo recordings as .fcast v3 and replay them in CI
Some checks failed
ci / fmt / clippy / test / deny / types / bundle (pull_request) Has been cancelled
0c33110b8b
Every recording under web/public/ predated felis's wire reset: its
frames spelled the old domain-serde shapes, so the gallery, the mobile
demo's replay tabs and the render bench showed an empty grid, and now
that the component reads felis-json v1 and refuses a pre-v3 header, a
console error too.

The files are rewritten in place rather than re-recorded. Several were
recorded from escape streams that no longer exist and from program
states (btm, tte) that a new recording would not reproduce, while the
old frames carry everything the new format needs: each variant maps
one to one onto its v1 DTO, `RehydrateBegin`'s dimensions move into
the `size` frame a producer emits beside the burst, and the header
becomes felis-fcast's v3 (`felis_json: 1`). The hand-authored glyph
sheet is regenerated from its generator, which now writes v3 itself.

`tests/fixtures.rs` replays every recording frame by frame through the
view, so the next format change that strands them fails `cargo test`
instead of only the demo page.

Assisted-by: Claude Code
natsukium changed title from deps: follow felis to 105b0899 (wire-baseline reset) to deps: follow felis to v0.1.0 2026-09-26 19:21:56 +09:00
ci: run on the x86_64-linux runner label
All checks were successful
ci / fmt / clippy / test / deny / types / bundle (pull_request) Successful in 1m28s
f491ca0af7
The Forgejo runners were re-registered with labels named after Nix
systems (nix -> x86_64-linux, macos -> aarch64-darwin), so jobs asking
for the old labels sit in the queue with no runner to pick them up.
deps(gateway): lock felis-web-gateway to main
All checks were successful
ci / fmt / clippy / test / deny / types / bundle (pull_request) Successful in 1m26s
5dec51a7e1
The gateway's v0.1.0 follow (felis-web-gateway#4) has merged, so the
input goes back to tracking main instead of the PR branch, which is
left behind by the squash merge.
natsukium referenced this pull request from a commit 2026-09-26 20:57:56 +09:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
natsukium/felis-web-component!3
No description provided.