deps: follow felis to 105b0899 (wire-baseline reset) #3

Open
natsukium wants to merge 4 commits from follow-felis-105b0899 into main
Owner

Needs work before merge: CI cannot be green. felis-client-core does not
build with default-features = false at this rev, which is the whole wasm
reuse path — natsukium/felis#195. Everything below was verified against a
felis checkout carrying the two-line fix that issue proposes.

Follows felis from 32775067 (2026-07-16) to
105b089979369fd310757375af3df7900ae7e523. felis reset its wire baseline to
1.0 on 2026-09-05, so this is a break-for-break adaptation, not a version bump.

What felis changed, and what it forced here

The shadow screen

ShadowGrid is ShadowScreen, constructed from (rows, cols) alone —
with_encoding went with the proto3-JSON leg, so the shadow always decodes the
packed row codec. A .fcast and the gateway both carry rows in the
structural-JSON view, so applyJson now recodes through
felis_grid::transcode::rows_to_wire before applying. Recoding rather than
teaching the view a second row encoding: the codec's version tag is what gates
a future layout, and only one side of a recode may know it.

ShadowScreen::screen() replaces grid(), apply no longer mirrors
OSC 10/11/12 (the view keeps the three channels itself for themeJson), and
kitty_kbd_flags() / modify_other_keys() return typed values.

The row codec

encode_row_with / decode_row_with split into encode_row / decode_row
(packed) and encode_row_json / decode_row_json, taking a RowEncode with
an explicit pad_to. The replay scrollback ring stores the packed form now —
nothing outside the ring reads those bytes, and it is about a third the size.

Message shapes

RowDelta carries rows: [(row, payload)] and absorbed RowDeltaBatch;
GridSize is Size { dims }; RehydrateBegin is payload-free. Image
transfers are canonical: Header carries an ImageTarget, total_bytes and a
chunk's offset are gone, ImageShadow::apply is fallible, 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, carrying text the daemon already resolved. The captureRow
wasm export decoded that payload and has no source left, so it goes; its
real-daemon row fixture moves onto applyJson, where those same bytes now
arrive as a RowDelta.

Capabilities

The per-feature bitmap is gone: a connection states one ConnectionMode and
negotiates a protocol minor. Attached.caps becomes Attached.minor and
caps() becomes protocolMinor(). Both transports already carry it — the
gateway's attach reply names minor, a .fcast v2 header names
proto_minor.

Search and export, removed from the browser's reach

Search and scrollback export left InputMsg in the same reset: they are
Search / Region requests now, correlated families whose ids the connection
driver allocates, and the gateway's broker routes neither direction of either
(felis-web-gateway docs/broker.md, "Stream frames"). So search(),
cancelSearch() and captureText() for anything but "visible" refuse
locally, and the reply plumbing they fed is removed.

Leaving the old calls would have been worse than useless: an undecodable frame
is no longer skipped, it ends the connection, so pressing search would have
cost the user their session. Tracked as #2 with the work needed to restore it.

Verified

Against a felis checkout patched with the natsukium/felis#195 fix (the three
modules gated behind native), by [patch]ing the four felis crates to local
paths — no repo file was changed to make these pass:

nix develop -c cargo fmt --all --check                                  ok
nix develop -c cargo clippy --all-targets --target wasm32-unknown-unknown -- -D warnings   ok
nix develop -c cargo test --target wasm32-unknown-unknown               25 + 9 passed
nix develop -c cargo deny check                                         ok
nix develop -c cargo build --lib --target wasm32-unknown-unknown --release
  + wasm-bindgen … --out-dir web/pkg --target web
  + nix develop -c tsgo -p . --noEmit                                   ok

Unpatched, against felis main as pinned, every cargo step fails in
felis-client-core and nothing downstream of it runs.

nix build .#web gets as far as the same felis-client-core failure; its
outputHashes are updated for the new rev and the vendoring resolves, so the
derivation is correct up to that point.

The bench harness is not a CI gate; its workload generator emits the new
RowDelta shape but has not been run.

Open

  • natsukium/felis#195felis-client-core does not build with
    default-features = false. Blocks every gate here. dev/flake-module.nix
    claims CI guards this configuration; no recipe or workflow actually builds
    it, which is how it rotted.
  • natsukium/felis#193 (filed by the gateway follow) — transcode's JSON
    view renders a RowDelta row as a byte array rather than a structural row.
    This branch decodes that shape correctly. If felis restores the inline
    RawValue splice, RowPayload stops deserializing from Vec<u8> and
    applyJson will need to follow.
  • #1 — every web/public/*.fcast predates the reset and no longer decodes,
    so the demo gallery is empty. Regenerating needs felis-fcast's own follow
    plus a live daemon.
  • #2 — search and scrollback export unreachable, per above.
> **Needs work before merge: CI cannot be green.** `felis-client-core` does not > build with `default-features = false` at this rev, which is the whole wasm > reuse path — natsukium/felis#195. Everything below was verified against a > felis checkout carrying the two-line fix that issue proposes. Follows felis from `32775067` (2026-07-16) to `105b089979369fd310757375af3df7900ae7e523`. felis reset its wire baseline to 1.0 on 2026-09-05, so this is a break-for-break adaptation, not a version bump. ## What felis changed, and what it forced here ### The shadow screen `ShadowGrid` is `ShadowScreen`, constructed from `(rows, cols)` alone — `with_encoding` went with the proto3-JSON leg, so the shadow always decodes the packed row codec. A `.fcast` and the gateway both carry rows in the structural-JSON view, so `applyJson` now recodes through `felis_grid::transcode::rows_to_wire` before applying. Recoding rather than teaching the view a second row encoding: the codec's version tag is what gates a future layout, and only one side of a recode may know it. `ShadowScreen::screen()` replaces `grid()`, `apply` no longer mirrors OSC 10/11/12 (the view keeps the three channels itself for `themeJson`), and `kitty_kbd_flags()` / `modify_other_keys()` return typed values. ### The row codec `encode_row_with` / `decode_row_with` split into `encode_row` / `decode_row` (packed) and `encode_row_json` / `decode_row_json`, taking a `RowEncode` with an explicit `pad_to`. The replay scrollback ring stores the packed form now — nothing outside the ring reads those bytes, and it is about a third the size. ### Message shapes `RowDelta` carries `rows: [(row, payload)]` and absorbed `RowDeltaBatch`; `GridSize` is `Size { dims }`; `RehydrateBegin` is payload-free. Image transfers are canonical: `Header` carries an `ImageTarget`, `total_bytes` and a chunk's `offset` are gone, `ImageShadow::apply` is fallible, 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`, carrying text the daemon already resolved. The `captureRow` wasm export decoded that payload and has no source left, so it goes; its real-daemon row fixture moves onto `applyJson`, where those same bytes now arrive as a `RowDelta`. ### Capabilities The per-feature bitmap is gone: a connection states one `ConnectionMode` and negotiates a protocol minor. `Attached.caps` becomes `Attached.minor` and `caps()` becomes `protocolMinor()`. Both transports already carry it — the gateway's attach reply names `minor`, a `.fcast` v2 header names `proto_minor`. ### Search and export, removed from the browser's reach Search and scrollback export left `InputMsg` in the same reset: they are `Search` / `Region` requests now, correlated families whose ids the connection driver allocates, and the gateway's broker routes neither direction of either (`felis-web-gateway` `docs/broker.md`, "Stream frames"). So `search()`, `cancelSearch()` and `captureText()` for anything but `"visible"` refuse locally, and the reply plumbing they fed is removed. Leaving the old calls would have been worse than useless: an undecodable frame is no longer skipped, it ends the connection, so pressing search would have cost the user their session. Tracked as #2 with the work needed to restore it. ## Verified Against a felis checkout patched with the natsukium/felis#195 fix (the three modules gated behind `native`), by `[patch]`ing the four felis crates to local paths — no repo file was changed to make these pass: ``` nix develop -c cargo fmt --all --check ok nix develop -c cargo clippy --all-targets --target wasm32-unknown-unknown -- -D warnings ok nix develop -c cargo test --target wasm32-unknown-unknown 25 + 9 passed nix develop -c cargo deny check ok nix develop -c cargo build --lib --target wasm32-unknown-unknown --release + wasm-bindgen … --out-dir web/pkg --target web + nix develop -c tsgo -p . --noEmit ok ``` Unpatched, against felis main as pinned, every cargo step fails in `felis-client-core` and nothing downstream of it runs. `nix build .#web` gets as far as the same `felis-client-core` failure; its `outputHashes` are updated for the new rev and the vendoring resolves, so the derivation is correct up to that point. The bench harness is not a CI gate; its workload generator emits the new `RowDelta` shape but has not been run. ## Open - **natsukium/felis#195** — `felis-client-core` does not build with `default-features = false`. Blocks every gate here. `dev/flake-module.nix` claims CI guards this configuration; no recipe or workflow actually builds it, which is how it rotted. - **natsukium/felis#193** (filed by the gateway follow) — `transcode`'s JSON view renders a `RowDelta` row as a byte array rather than a structural row. This branch decodes that shape correctly. If felis restores the inline `RawValue` splice, `RowPayload` stops deserializing from `Vec<u8>` and `applyJson` will need to follow. - **#1** — every `web/public/*.fcast` predates the reset and no longer decodes, so the demo gallery is empty. Regenerating needs `felis-fcast`'s own follow plus a live daemon. - **#2** — search and scrollback export unreachable, per above.
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.

d2c915dfix(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.

02c6764fix(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.
Some checks failed
ci / fmt / clippy / test / deny / types / bundle (pull_request) Failing after 14s
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin follow-felis-105b0899:follow-felis-105b0899
git switch follow-felis-105b0899

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff follow-felis-105b0899
git switch follow-felis-105b0899
git rebase main
git switch main
git merge --ff-only follow-felis-105b0899
git switch follow-felis-105b0899
git rebase main
git switch main
git merge --no-ff follow-felis-105b0899
git switch main
git merge --squash follow-felis-105b0899
git switch main
git merge --ff-only follow-felis-105b0899
git switch main
git merge follow-felis-105b0899
git push origin main
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.