v0.6.90b420.dev732
The TUI's navigation is rebuilt around one key vocabulary. The footer fits an 80-column terminal again. Placement now says no honestly instead of loading a model that cannot answer. The x86 engines use the CPU you actually have, and brew install works on an Intel Mac.
The catalog keeps your place
Arrow keys stranded the cursor. Card highlighting flickered. On Discover the selection walked off-screen and never came back.
The grid now re-locates the highlighted row by identity when a background page lands. Discover is a real scroll container, and its arrow keys drive the visible tab. Crossing sections moves focus between grids on purpose.
One set of keys, everywhere
q and Escape go back to the view you came from. They no longer dump you in Chat. m opens the catalog directly.
Eight screens share one key vocabulary. Sessions gains Escape and list keys. TaskCenter gains g and G. Settings stops naming the same keys differently.
A footer that fits
The old footer packed ten app keys behind three group labels. It rendered as tmc^g^oViews. Five destinations and three unrelated actions sat behind one word each, so nothing said which key went where.
The footer now carries only what moves you between views. Each key gets its own word. The screen's one verb sits beside them. Rendered widths drop from 131-190 columns to 66-93, so the row fits an 80-column terminal.
Everything else moved to the help panel. ? opens it. F1 and Ctrl+H are gone. tmux and terminal emulators intercept function keys, and many terminals send Ctrl+H for Backspace. That put a help binding one keystroke from every text field.
Clicking a session row no longer crashes the app
MouseDown on a session row raised an AttributeError. Textual's text-selection path reads a container region without a None check. Rows lose their parent on every store mutation and every filter keystroke, so the window was open often.
Row text now opts out of selection. Textual checks that before it enters the path, so the crash cannot happen.
A fuzz harness, and what it caught
make fuzz-smoke drives seeded-random adversarial sessions. It sends random keys, view bursts, resize storms, random clicks, Escape spam, and unicode paste. It asserts structural invariants after every step. Failures replay by seed. CI runs it as a job.
Its first catch: a 20-column resize with the fleet drawer open squeezed a completion overlay to zero width. Textual's height measurement then took down the app.
One measured optimization
A real-terminal py-spy flame graph put ssl.create_default_context at 16% of a whole TUI session. Catalog model discovery built a fresh httpx Client for every localhost probe, on every tab activation. Each one loaded the full CA bundle.
Discovery now shares one client, the same way the engine probes do. A construction costs 6.42 ms; the shared client costs almost nothing. Full write-up in docs/benchmarks/tui-profiling.md.
Measured, not asserted
scripts/qa/tui_nav_eval.py drives the real TUI headlessly. It runs one scenario per improvement against both releases.
| Scenario | dev731 | dev732 |
|---|---|---|
| Catalog cursor survives a background data refresh | ❌ cursor 2 -> None after refresh, next arrow lands 0 | ✅ cursor 2 -> 2 after refresh, next arrow lands 3 |
| Discover: arrows never lose the cursor | ❌ cursor lost on press 1: focus went to ModelGrid | ✅ 10 downs stayed on visible rail grids; the up-walk returned to the first rail |
| q returns to the view you came from | ❌ Settings -> Catalog -> q landed on ChatScreen | ✅ Settings -> Catalog -> q landed on SettingsScreen |
| A direct key opens the model catalog | ❌ pressing m on Settings landed on SettingsScreen | ✅ pressing m on Settings landed on CatalogScreen |
| Data changes repaint the catalog when the row shape is unchanged | ❌ repaint after data change: False | ✅ repaint after data change: True |
| A never-installed chat model is named | ❌ "The engine is not ready yet. Send your prompt again in a moment." | ✅ "The engine failed to load: chat model Test Q4_K_M.gguf is not installed" |
| Re-adding an indexed file is a no-op | ❌ confirm dialog opened | ✅ added directly, no dialog |
| Tab to No then Enter cancels the dialog | ❌ dialog returned True | ✅ dialog returned False |
A model that cannot answer is refused
A chat model can load, split across cards, and report ready. Its fitted context can still collapse to the 512-token floor. The generation reserve alone is 1152 tokens, so every retrieval prompt then failed at query time.
The planner now drops such a launch and gives the numbers: it serves N tokens and needs about M. Use a smaller model or quant. An explicit num_ctx pin still serves as asked. A small window you chose is a choice, not a collapse.
Tight splits are sized per card
A best-effort placement hands a model several cards without a tensor-split ratio. This is deliberate, because llama.cpp then runs its own fit pass. That ratio was also the only signal for how many devices to size against.
So a 70B on two 24 GiB cards was charged 39.9 GiB against 20.8 GiB of headroom. Every context probe failed down to the floor. The fit now derives the ratio from the per-device free bytes it already receives.
Validated on 2x4090 with Llama 3.3 70B Q4_K_M. It serves 5888 tokens with no pin. VRAM stayed flat across five consecutive full-window requests.
x86 engines pick their CPU variant at startup
The x86 builds shipped their engine at a hard AVX baseline. This kept pre-AVX2 machines from crashing, but it cost every Haswell and later CPU its AVX2, FMA, and F16C.
The macOS x86_64, Linux, and Windows CPU cells now ship one CPU backend per x86 feature level. The Linux Vulkan cell does too. Each loads the best match at backend init. On a 2015 Broadwell MacBook Pro: prompt processing 14.6 to 34.1 tok/s, generation 11.7 to 17.6. The x64 and sse42 variants sit below the old AVX floor, so pre-AVX hardware stays covered.
Vulkan Windows, CUDA, ROCm, and SYCL keep the AVX cap. Each one waits for validation on its own hardware, and cmake_args.sh records the reason per cell. The Vulkan Linux bundle carries the SDK's loader, because a missing loader falls back to CPU silently instead of failing at startup. The build rejects any dispatch bundle with fewer than two variant modules.
brew install works on Intel Macs
The Homebrew formula published only an arm64 url. brew install failed on every Intel Mac. It now publishes an Intel macOS url too.
Switching models mid-answer keeps the answer
A switch during a streaming answer cancelled the stream. It also disabled the chat input for the whole fleet restart, so the app looked dead.
The switch is now queued and applied when the stream ends. A toast names the model. Nothing in flight is discarded.
Slash commands were also refused during a stream. That made /cancel dead exactly when you wanted it. /cancel and /model now run during a stream. Anything that touches the index, the fleet, or the transcript stays gated.
Downloads run over Xet
Xet was disabled outright. It reported bytes only when a buffered block flushed to disk, so the progress bar looked stuck.
huggingface_hub reports a second byte stream for this. The bar now reads it: 1499 progress events against 6, and a 0.10s median gap against 15.0s.
That puts models back on the fast transport. Xet measured about 14x faster than HTTP on a cold 1GB download, well outside the 9.8% noise floor. Cancelling a download now stops the transfer. Before, it finished behind a row that read cancelled.
The flatpak you download installs
Every channel fetches its binary as extra data. flatpak build-bundle does not carry that pointer into a bundle. All four .flatpak files were empty shells and failed to install.
Releases now ship a .flatpakref. It carries the remote instead of the payload. It holds no version, so one file stays valid across releases. The smoke test installs the rendered file, so an artifact that cannot be installed fails the publish job.
Layout detection is opt-in
Layout-aware PDF extraction was on by default. Enabling it downloads the ONNX layout and table-structure models. It also adds per-page inference, which a CPU-only ingest pays for on every document.
Set LILBEE_LAYOUT_DETECTION=1 when you want it. Table extraction needs it for the structure model. Without it, tables fall back to the native extractor, and the docs now say so.
Missing engine, honest message
An install without the [engine] extra has no llama-server. Ingest reported this as "No embed model server is running. Make sure a embed model is installed and configured". Every word pointed at a model that was already installed and correct.
Chat had the same hole. It rendered "the engine is not ready yet" for the rest of the session.
Both now name the missing engine. Both carry the install command for your hardware. A chat model that was never installed is named too, instead of retrying forever.
Text file I/O names its encoding
Text I/O without an encoding decodes with the machine's locale. A path or model name outside cp1252 then raises on Windows and passes everywhere else.
Twenty-three sites now name utf-8. Eight except clauses promised a default on an unreadable file and crashed instead; they no longer do. A style gate keeps new code from reintroducing it.
Security
aiohttp moves to 3.14.3 and cryptography to 50.0.0. Together they close eight open alerts: an out-of-bounds heap read in the C HTTP response parser, two WebSocket issues, and a PKCS#7 decryption oracle.
Docs
Install docs put the bundled builds first and pip last, labelled for developers. A new section explains why the engine is not on PyPI. The engine install hint gives one command per hardware, because the five indexes are not interchangeable.