- Video: probe the MP4 header (range request for moov) before downloading the whole clip; if the average bitrate overruns the declared H.264 level the Vita's decoder enforces, show the "too high" message up front instead of downloading then failing at playback. Verdict validated against the real demo videos (e.g. e75d495e: Level 3.1 @ 18.8 Mbps -> rejected). - Error screens: word-wrap title + detail so long messages no longer run off the edges; the bitrate message now spells out the server-side fix (Immich transcoding policy / bitrate cap). - Boot: one continuous spinner. A single boot worker thread does sign-in + first page + local scan while the main thread animates one logo with a never-resetting frame counter, instead of three separate static screens. - Throbbers: the cloud/server-info page and smart search now fetch on a worker thread and animate a throbber instead of freezing the UI. - Logo: swap the in-app logo for a clean 200x200 transparent PNG (the old one was matted from the white-background icon and had fringe); keep the launcher icon's white background. Linear-filter the logo + cloud badges so they scale/rotate smoothly. - Drop AgenticLoop.md (and its CLAUDE.md reference). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
7.2 KiB
CLAUDE.md — vitaImmich
PS Vita Immich client (PoC). C sources in src/. It's a unity build:
main.c holds the includes, #defines and core globals, then #includes the
section files (state.c, draw.c, config.c, net.c, jpeg.c, api.c,
thumbs.c, display.c, ui.c, video.c, sync.c, app.c) in order. They
form one translation unit and share all file-scope state — there are no
headers or extern decls. Only src/main.c is compiled (CMakeLists lists
just it); the parts are textual slices, so add new code to the relevant part,
not to CMake. jsmn.h is vendored.
Built with the VitaSDK toolchain into a .vpk, run in the Vita3K emulator,
and driven headlessly through the kwin-mcp server. This file is the
end-to-end loop: build → install → launch → drive → read logs. Full key
map is in controls.MD.
App title id: VIMM00001.
1. Build the changes → a .vpk
After editing anything in src/ (or CMakeLists.txt), rebuild.
Preferred — Docker (reproducible), writes build/vitaImmich.vpk:
./build.sh
build/ends up root-owned. If the Docker runtime is broken (containerd shimreadlink /proc/self/exe: no such file or directory), use the native fallback.
Fallback — native host SDK, also writes build/vitaImmich.vpk:
export VITASDK="$HOME/vitasdk"
export PATH="$VITASDK/bin:$PATH"
cmake -B build -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_TOOLCHAIN_FILE="$VITASDK/share/vita.toolchain.cmake" .
cmake --build build --parallel "$(nproc)"
If a prior Docker build left
build/root-owned, clear it first (sudo rm -rf build) so the native build can write there.
Whichever path you use, the next step installs that .vpk.
2. Install the rebuilt VPK into Vita3K
Installing = launch Vita3K once with the .vpk as an argument; it extracts and
auto-boots. deploy.sh does install-then-launch in one shot:
./deploy.sh # installs ./build/vitaImmich.vpk, then launches VIMM00001 detached
Gotchas (these are why it must run from a script, not inline):
-
Background it from a script file with a
sleep/waitthenpkill. Inline backgrounding from the agent's shell does not persist. -
Guard
pkillwith|| true— a no-match exit code aborts the chain. -
Vita3K preserves the zip's internal mtime on extract. To confirm a fresh install, compare the installed
eboot.binmtime against the vpk entry, not wall-clock:unzip -l build/vitaImmich.vpk | grep eboot stat -c 'eboot: %y' ~/.local/share/Vita3K/Vita3K/ux0/app/VIMM00001/eboot.bin -
Wipe a stale install:
vita3k -d VIMM00001.
deploy.shlaunches detached on the live desktop. For agent-driven verification with screenshots, skipdeploy.sh's launch and instead launch inside the MCP's isolated session (step 3) — only do the install half first (vita3k ./build/vitaImmich.vpkfor ~4s, thenpkill).
3. Launch + drive via kwin-mcp (screenshots + keystrokes)
Launch the app inside kwin-mcp's own isolated KWin session — its screenshot
tool only works there. session_connect to the live desktop does not capture
(spectacle can't save non-interactively; KWin's ScreenShot2 DBus rejects
unauthorized callers). The isolated session is headless: the human cannot watch
it live, which is exactly why we capture every frame to disk.
-
Wipe the per-session frame folder first:
rm -rf agent-run/frames && mkdir -p agent-run/frames. (agent-run/is git-ignored;frames/is the throwaway capture folder — your curated keepers stay inagent-run/proper and are never touched.) -
session_startwithapp_command: "vita3k -r VIMM00001",keep_screenshots: true, andenv: {"SDL_VIDEODRIVER": "wayland", "DISPLAY": ""}— starts the isolated session, launches the app, and keeps every captured frame aftersession_stop. Give it ~12s to boot.The
envis mandatory. The session inherits the host'sDISPLAY=:1, so without it Vita3K's SDL backend picks X11 and renders to the host display — the captured frames come back blank white,focus_window "Vita3K"fails ("no application matching"), and keystrokes never reach the app. ForcingSDL_VIDEODRIVER=waylandand clearingDISPLAYmakes Vita3K render into the MCP's Wayland session, where capture and input both work. Sanity check:list_windowsshould show aVita3Kwindow and frames should be ~300–500 KB, not ~8 KB. -
focus_windowwithapp_name: "Vita3K"BEFORE every key. Without focus, keypresses are dropped. They also drop intermittently — re-focus and re-press. One key per call; rapid bursts get coalesced/dropped. -
keyboard_keywithscreenshot_after_ms: <ms>after every command, so the frame folder is a complete visual record. -
session_stopwhen done, thenpkill -9 -f vita3k. kwin-mcp always writes frames to/tmp/kwin-mcp-screenshots-*/(not configurable) — move them into the repo and clear tmp, then tell the human where they are:cp /tmp/kwin-mcp-screenshots-*/*.png agent-run/frames/ 2>/dev/null rm -rf /tmp/kwin-mcp-screenshots-*
Key map (this app's actions; full list in controls.MD)
| Vita button | host key (kwin-mcp) | in-app action |
|---|---|---|
| D-pad | Right Left Up Down |
move grid / browse |
| Cross (X) | x |
view / play / replay |
| Circle (O) | c |
back |
| Select | Shift_R |
sync |
| Start | Return |
exit |
4. Read the logs — verify from TEXT, not frames
Capturing a frame is free (PNGs on disk are not in context). Reading a PNG back into context is the expensive part. So capture every command, but do not Read frames back during the loop. Verify progress from the app's own text log instead:
~/.local/share/Vita3K/Vita3K/ux0/data/vitaimmich/log.txt
This is the app's own log — it records page fetches, video open/play, scans, and errors. Also available:
- MCP
read_app_logwith the launched PID. /tmp/vita3k-runtime.log(Vita3K runtime stdout/stderr fromdeploy.sh)./tmp/vita3k-install.log(install output).
Only Read a frame when a screenshot is genuinely the only way to resolve an ambiguity (rare). Reading every frame is what bloats context — that's what this rule avoids.
Typical verify loop
- Build (step 1) → install half of step 2.
rm -rf agent-run/frames && mkdir -p agent-run/frames, thensession_start(keep_screenshots: true,envforcing Wayland) → wait ~12s.focus_window "Vita3K".keyboard_key "Right"(+screenshot_after_ms), stepping to the target tile, one key per call;xto open,xagain to play.- Confirm what happened from
log.txt— not the frames. session_stop+pkill; move frames into the repo and clear tmp (cp /tmp/kwin-mcp-screenshots-*/*.png agent-run/frames/ 2>/dev/null && rm -rf /tmp/kwin-mcp-screenshots-*), then handagent-run/frames/to the human as the visual record.