My multiplexer lied about what it could do
Images worked in the terminal, worked in the multiplexer, worked in the browser client, and failed the moment you stacked all three. The middle layer was answering a capability question it had never asked anyone.
Gaurav Gosain
Images displayed fine in my terminal. Fine in the multiplexer running inside
it. Fine in the browser client. Run all three at once, browser client hosting a
shell hosting the multiplexer, and icat produced nothing at all. No error, no
placeholder, no partial image, just the prompt sitting where the image should
have been.
What icat is actually doing
The kitty graphics protocol has a capability query. Before sending anything
real, icat asks the terminal whether it supports a given transmission medium,
and it asks about three of them:
_Ga=q,f=24,s=1,v=1,S=3,i=1 can you take pixels inline
_Ga=q,f=24,t=t,s=1,v=1,S=47,i=2 can you read a temp file
_Ga=q,f=24,t=s,s=1,v=1,S=18,i=3 can you read shared memoryIt commits to whichever answers first with OK, which is sensible. A file is
much cheaper than base64 inline, so if the terminal can read files, everyone
wins.
The lie
The multiplexer's passthrough answered OK to all three. Unconditionally.
response := vt.BuildKittyResponse(true, cmd.ImageID, "")
ptyInput(response)It never asked its own host anything. It just said yes.
So icat picked a file medium, wrote the image to /tmp, and sent a path. The
multiplexer forwarded that path outward, to a browser, which cannot read /tmp
on the server, so it dropped the transmission and drew nothing.
Captured at the browser, the failure looks like this:
a=t,t=f,i=1,f=100,s=64,v=64,q=2,c=8,r=4payload=132 "L3RtcC9jbGF1ZGUtMTAwMC8t"a=p,i=1,p=1,c=8,r=4,q=2 place image 1images:0 placements:0 canvases:0
Why each layer alone was fine
In a real terminal, the host genuinely can read the path, so the lie is true by accident.
In the browser client alone, nothing lies. The client answers ENOTSUPPORTED
to the file media, icat falls back to streaming the bytes inline, and
everything works.
To hit the bug you need a dishonest middle layer sitting in front of a host that cannot do the thing, and that only happens in the three-layer stack. Each component was correct in the environment its author tested it in, which is what made staring at any one of them so unproductive.
The fix
Probe the host for file transmission alongside the direct probe, record the answer, and pass it on:
if isFileMedium(cmd.Medium) && !kp.hostReadsFiles() {
ok = false
errMsg = "ENOTSUPPORTED:host terminal cannot read files from this machine"
}A guest that never asks is covered too. If a file transmission arrives and the host cannot read files, the multiplexer re-encodes it as direct data rather than forwarding a path that will be silently discarded. A file-capable host still gets the plain path with no extra copy, so the fast case stays fast.
There was a second bug hiding in the same function. Quiet mode has two levels,
q=1 to suppress success responses and q=2 to suppress everything, and the
code treated both as "say nothing". So a guest that asked for q=1 and hit an
error got silence, waited out its timeout, and never tried the next medium. An
error has to reach a guest that only suppressed successes.
Ask, or say no
A proxy answering capability questions on behalf of something else has two honest options: ask the far end, or say no and let the guest fall back to the medium that always works. Saying yes because yes usually works is how you get a failure that only appears in composition, only for people running unusual stacks, with nothing logged anywhere and every individual component pointing at its own passing tests.
The part that actually annoys me is that the multiplexer already had the machinery to handle this. A function that re-encodes file transmissions as inline data was sitting in the codebase, gated behind a build-specific flag rather than behind "can the host actually read files". I did not so much write the fix as move it.