Skip to content

os/puppy: full-screen console TUI (ratatui) replaces the dialog menu - #921

Merged
bong-water-water-bong merged 5 commits into
mainfrom
os/v3-console-tui
Nov 3, 2026
Merged

bong-water-water-bong merged 5 commits into
mainfrom
os/v3-console-tui

Conversation

@bong-water-water-bong

@bong-water-water-bong bong-water-water-bong commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Cycle 18 item 7 + item 4 (owner): the dialog console menu on tty1 becomes a full-screen TUI, and the top of
the screen is the CONNECT block the cycle-18 board boot asked for ("when you first boot the headless there is
no data to display on how to connect, just the menu"
).

Brief: ~/briefs/TCONSOLE-TUI.md. Built and measured on strixhalo; no image was built by this lane (pi-os3
cycle 18 owns the build, and the recipe takes the binary as CONSOLE_TUI_BIN).

Coordination with cycle 18 (os/v3-c18, 3a428849)

That branch adds the same CONNECT block to the dialog menu and the U2 menu connect row. This branch was
test-merged against it: no conflict (1bit-console-menu: they change screen_text()/main_menu, this adds
--action in the argument dispatch; git merges both cleanly - measured, then aborted). After they land, the row
reads green on either surface.

The --action flag is the only edit to 1bit-console-menu here; --status, --run, --selftest and the status
block are untouched, and U2 menu/U2 menu status/U2 menu actions/U2 menu env keep passing.

What it is

os/v3/puppy/console-tui/ — one Rust binary (ratatui + crossterm), x86_64-unknown-linux-musl, 1,465,880
bytes, readelf PT_INTERP absent, ldd says "statically linked"
. No runtime dependency: the appliance has no
Rust toolchain and must never fail to start a console over a loader.

build-static.sh runs cargo test, builds the musl target and refuses a dynamic result. container-build.sh
installs it when CONSOLE_TUI_BIN is set:

CONSOLE_TUI_BIN build
unset note: tty1 keeps the dialog menu (the documented fallback), build continues
a path that does not exist REFUSED
a binary that does not run (--version fails) REFUSED
the musl binary installed, and the packed SFS must show it -rwxr-xr-x or the build is REFUSED

All four branches measured here by running the recipe's own block. 1bit-console tries the TUI first and execs
the dialog menu on any exit, zero or not — measured for all three cases (TUI exits 0 / exits 7 / is absent: the
menu always follows, so no path leads to a bare prompt).

The screen

Two layouts off ONE set of facts (src/ui.rs), so the two cannot disagree:

  • 240x67 (tty1 on a monitor): header, CONNECT block, six panels in a 3x2 grid, a tall log pane, the nine
    actions on two rows.
  • 80x25 (serial/VGA default, what the QEMU rows read): the same block and panels one line each, a shorter log
    pane, and the same action labels — Show the API key and Reboot / power off are never abbreviated,
    because vm-check.sh's U2 menu row looks for exactly those two strings on tty1.

Both frames are committed as regression fixtures (tests/snapshots/*.txt, compared byte for byte) and are below.

CONNECT block (cycle 18 item 4)

Composed on every draw from live facts, and in the dialog menu's own wording (cycle 18's screen_text() on
os/v3-c18), because that branch's new U2 menu connect row greps CONNECT, web UI http://, api http://,
ssh root@ and Lemonade Mobile off tty1's VGA buffer — the row has to pass whichever of the two surfaces tty1
gets, so the TUI renders the same four lines with the same two spaces, and a test renders 240x67 and 80x25 and
asserts all five strings plus the address regex are on screen. While the box is still coming up the title carries the state and a
spinner (CONNECT - waiting for network |, then CONNECT - Lemonade starting /), the facts refresh every 2 s
instead of 3 s until both answer, and with no address the block says no network yet - press 3 to renew DHCP, set a static address or the hostname.

Panels

Lemonade (up/pid/URL/key hint), GPU (the runtime's device + free MiB, VRAM/GTT used, temp), NPU (amdxdna
loaded, firmware present, accel0 busy/idle), Models (downloaded, loaded), Fans/temps, Data (total/free) and the
last request's tok/s + time to first token from GET /api/v1/stats. Log pane: tail of Lemonade's log plus
dmesg filtered to amdgpu/amdxdna/hrx/accel, wrapped (nothing is cut — the c16 board's firmware prese is a
test that walks widths 8..60) and scrollable; 8 is full screen.

Keys

1 API key (confirmed first — it is a bearer token and this screen can be on a KVM recording) · 2 chat test ·
3 network · 4 models · 5 install · 6 desktop · 7 shell · 8 logs · 9 reboot/power off (confirmed). Arrows/PgUp/PgDn
scroll, f follows, r re-reads now, q stays on this screen.

Keys 2-7 hand the terminal to 1bit-console-menu --action N (new argument: one item, interactively). That is
deliberately the same code path U2 menu actions and --selftest already exercise, so the TUI cannot drift from
the menu and an action the gate proves is an action the screen has. Nothing that needs a typed answer is
re-implemented.

The GPU probe, and why it is not the menu's bug again

facts::lemonade_env() sources /etc/1bit/libhsa.env (rather than re-implementing its
${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}) and hands that environment to llama-server --list-devices, because an
/etc/inittab respawn inherits neither the loader path nor the no-SVM trio — the cycle-13 board boot said "GPU no
HRX device" while chat-hrx served 303 tok/s. A connect to 127.0.0.1:13305 (not a netstat listing) decides
whether Lemonade is up: measured on this box, netstat absent, so a listing-based answer said "not listening"
while curl answered 200.

Tests — 25, cargo test

Parsers against recorded samples, not invented ones: the board's own
HRX0: AMD Radeon 8060S Graphics (Node 1) (gfx1151) (122880 MiB, 74158 MiB free), the (none) of runs A and D of
the libhsa measurement, the efivar bytes 06 00 00 00 01, the documented /v1/models / /v1/health /
/v1/stats bodies, chunked and plain HTTP, df, the c16 status lines, wrap_words at every width. Plus the two
render snapshots below and a no-panic pass at 40x12 and 240x8. --status prints the same facts as plain text (a
gate row can read it without a terminal), --snapshot WxH [--fixture] renders one frame.

tools/copyright.py --check rc 0 (the two snapshot frames are EXEMPT: a notice line would change the frame they
exist to freeze).

Not done here, on purpose

  • No vm-check.sh row. Cycle 18's PR edits that file; a second edit from this branch would conflict. Suggested
    row for whoever lands second: 1bit-tty1 1 | grep -q 'CONNECT' + 1bit-console-tui --status containing
    Lemonade GPU NPU Fans Data Models. The existing U2 menu / U2 menu status / U2 menu actions rows keep
    passing unchanged (the labels are the menu's, and the menu's --status/--selftest code is untouched).
  • No image build and no stick write from this lane.
  • The --action addition to 1bit-console-menu is 6 lines and is the only change to that file.

The two renders

240x67

1bit OS rc-2026-11-03-c16 strix  192.168.50.110  up 2h 13m  Secure Boot enabled  SoC 43C
 CONNECT ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  web UI   http://192.168.50.110:13305
  API base http://192.168.50.110:13305/api/v1
  API key  press 1
  ssh      root@192.168.50.110
  mobile   Lemonade Mobile: server http://192.168.50.110:13305, key from 1
 LEMONADE ---------------------------------------------------------------------- GPU --------------------------------------------------------------------------- NPU -------------------------------------------------------------------------
up (pid 8882)                                                                   HRX0: AMD Radeon 8060S Graphics                                                 amdxdna loaded
http://192.168.50.110:13305                                                     74158 MiB free                                                                  firmware present
API key: set, press 1                                                           VRAM 2G of 120G used, GTT 512M of 61G                                           accel0 idle
                                                                                43C
 MODELS ------------------------------------------------------------------------ FANS / TEMPS ------------------------------------------------------------------ DATA ------------------------------------------------------------------------
2 downloaded                                                                    fans: controlled (fan1 0 rpm fan2 0 rpm)                                        58G total, 52G free
Qwen3-0.6B-NPU-1bit                                                             temps max 43C                                                                   last request 330.8 tok/s, first token 10 ms
qwen3-0.6b-q4_k_m-1bit                                                                                                                                          4 requests served
loaded: qwen3-0.6b-q4_k_m-1bit
 LOGS newest -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
KRN [ 1.100000] amdgpu 0000:c7:00.0: HRX runtime registered HRX0
KRN [ 1.200000] amdxdna 0000:c7:00.1: firmware loaded
LEM 2026-11-03 12:07:38 llama-server: listening on 127.0.0.1:8001
LEM 2026-11-03 12:07:41 lemond: POST /api/v1/chat/completions 200 (327 tokens, 330.75 tok/s)










































1 Show the API key   2 Quick chat test (GPU and NPU)   3 Network (DHCP, static, hostname)   4 Models (list, pull, delete)   5 Install 1bit OS to a disk   6 Start the desktop (JWM) for this session   7 Shell (bash, returns here on exit)
8 Logs (Lemonade, kernel)   9 Reboot / power off
1-9 actions arrows scroll f newest r refresh 8 logs q stays on this screen

80x25

1bit OS rc-2026-11-03-c16 strix  192.168.50.110  up 2h 13m  Secure Boot enabled
 CONNECT -----------------------------------------------------------------------
  web UI   http://192.168.50.110:13305
  API base http://192.168.50.110:13305/api/v1
  API key  press 1
  ssh      root@192.168.50.110
  mobile   Lemonade Mobile: server http://192.168.50.110:13305, key from 1
LEMONADE up (pid 8882) http://192.168.50.110:13305 API key: set, press 1
GPU HRX0: AMD Radeon 8060S Graphics 74158 MiB free VRAM 2G of 120G used, GTT
  512M of 61G 43C
NPU amdxdna loaded firmware present accel0 idle
MODELS 2 downloaded Qwen3-0.6B-NPU-1bit qwen3-0.6b-q4_k_m-1bit loaded:
  qwen3-0.6b-q4_k_m-1bit
FANS / TEMPS fans: controlled (fan1 0 rpm fan2 0 rpm) temps max 43C
DATA 58G total, 52G free last request 330.8 tok/s, first token 10 ms 4 requests
  served
 LOGS newest -------------------------------------------------------------------
LEM 2026-11-03 12:07:41 lemond: POST /api/v1/chat/completions 200 (327 tokens,
  330.75 tok/s)
1 Show the API key   2 Quick chat test (GPU and NPU)
3 Network (DHCP, static, hostname)   4 Models (list, pull, delete)
5 Install 1bit OS to a disk   6 Start the desktop (JWM) for this session
7 Shell (bash, returns here on exit)   8 Logs (Lemonade, kernel)
9 Reboot / power off
1-9 actions arrows scroll f newest r refresh 8 logs q stays on this screen

os/v3/puppy/console-tui: one musl-static Rust binary (ratatui + crossterm)
that /etc/inittab's tty1 gets instead of the dialog menu. The owner asked for
it twice: 'can we make that a tui instead of this style' (cycle 18 item 7),
then, on the c18 board boot, 'when you first boot the headless there is no
data to display on how to connect, just the menu' (cycle 18 item 4).

The first block is CONNECT and it is composed from live facts on every draw:
IP(s) + hostname, the web UI, the OpenAI-compatible base, 'API key: press 1',
ssh root@<ip>, Lemonade Mobile; while booting it says 'waiting for network' /
'Lemonade starting' with a spinner and refreshes every 2 s until both answer,
and with no address it says so and points at key 3.

Panels: Lemonade, GPU, NPU, Models, Fans/temps, Data + the last request's
tok/s. Log pane tails Lemonade's log and dmesg's amdgpu/amdxdna lines, wrapped
so nothing is cut ('firmware prese', c16) and scrollable; key 8 is full screen.

Two layouts off one set of facts: 240x67 (tty1 on a monitor) and 80x25, where
the nine action labels are unchanged on purpose - vm-check's U2 menu row looks
for 'Show the API key' and 'Reboot / power off' on tty1.

Keys 2-7 hand the terminal to '1bit-console-menu --action N' (new, one item
interactively), i.e. the same code U2 menu actions/--selftest already exercise,
so the TUI cannot drift from the menu and the gates keep covering both. 1, 8,
9 and the log keys are native; q never exits to a bare prompt.

The GPU question is asked of the runtime (llama-server --list-devices) with
/etc/1bit/libhsa.env sourced, because an inittab respawn inherits neither
LD_LIBRARY_PATH nor the no-SVM trio - the cycle-13 board boot said 'no HRX
device' while chat-hrx served 303 tok/s. The env is obtained by sourcing that
file rather than re-implementing its expansion.

container-build installs it from CONSOLE_TUI_BIN (a Rust toolchain has no
business in the build container) and REFUSES a binary that does not run here
('--version'), a dynamic one, or a packed copy that is not executable; its
absence is only a note and tty1 keeps the dialog menu. 1bit-console execs the
menu on any exit, so there is no path from either screen to a bare prompt.

Tests: 25, including the parsers against recorded samples (the board's own
HRX0 line, the (none) of the libhsa measurement, the efivar bytes 06 00 00 00
01, the documented /v1/models|health|stats bodies, chunked HTTP, df, the c16
status lines) and render snapshots at 240x67 and 80x25 compared byte for byte
with tests/snapshots/, so a layout change cannot happen by accident.

tools/copyright.py: the two snapshot frames are EXEMPT (a notice line would
change the frame they exist to freeze). --check rc 0.
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

PR Reviewer Guide 🔍

(Review updated until commit 1a5dc31)

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Potential GPU Memory Parsing Issue

The gpu_mem_in function in facts.rs attempts to parse VRAM information from DRM card directories. If a card directory exists but lacks the expected memory files, the function may return default values (0) instead of properly handling the missing data, potentially leading to incorrect GPU memory reporting. This could be problematic if the system has cards that do not report VRAM.

pub fn gpu_mem_in(drm: &str, gpu: &mut Gpu) {
    let Ok(rd) = fs::read_dir(drm) else { return };
    let mut cards: Vec<std::path::PathBuf> = rd
        .flatten()
        .filter(|e| e.file_name().to_string_lossy().starts_with("card"))
        .map(|e| e.path())
        .collect();
    cards.sort();
    for card in cards {
        let base = card.join("device");
        let p = |n: &str| base.join(n).to_string_lossy().into_owned();
        let total = sysfs_u64(&p("mem_info_vram_total"));
        if total == 0 {
            continue;
        }
        gpu.vram_total = total;
        gpu.vram_used = sysfs_u64(&p("mem_info_vram_used"));
        gpu.gtt_total = sysfs_u64(&p("mem_info_gtt_total"));
        gpu.gtt_used = sysfs_u64(&p("mem_info_gtt_used"));
        return;
    }
}
Possible Log Scroll Bug

In the log_lines function, the logic for determining the scroll position of logs may not correctly handle edge cases where a.log_scroll exceeds the total number of log entries. This could lead to incorrect display of log content or panic when accessing log entries.

let end = total.saturating_sub(a.log_scroll.min(total.saturating_sub(1)));
// collected BACKWARDS and stopped as soon as the pane is full: drawing the log then costs what is on screen
Binary Verification Logic

The verification logic for CONSOLE_TUI_BIN in container-build.sh checks the version string against a hardcoded prefix "1bit-console-tui*". If this prefix changes in the future, the build process will fail even if the binary is valid. This could lead to unnecessary build failures.

case $tuiv in
    1bit-console-tui*) ;;
    *) echo "build: REFUSED: the console TUI printed '$tuiv' for --version" >&2; exit 1;;
esac

⚠️ Review coverage: The following files were not included in this review because of the token budget:

  • os/v3/puppy/console-tui/src/main.rs
  • os/v3/puppy/console-tui/README.md
  • os/v3/puppy/console-tui/tests/snapshots/80x25.txt
  • os/v3/puppy/embed/usr/local/bin/1bit-console
  • os/v3/puppy/console-tui/Cargo.toml

…wording

pi-os3's cycle-18 branch (os/v3-c18, 3a42884) adds the CONNECT block to the
dialog menu AND a vm-check row that greps it off tty1's VGA buffer:

    U2 menu connect  ... for w in CONNECT 'web UI http://' 'api  http://'
                     'ssh  root@' 'Lemonade Mobile'

Those greps are on the SCREEN, so the moment this TUI is what tty1 gets they
would go red on a block that says the same thing in different words. The TUI
therefore renders the menu's block verbatim - same four lines, same two spaces
in 'api  http://' and 'ssh  root@' - and a new test renders both console sizes
and asserts every one of those strings (plus the address regex) is on screen.
A row that fits is emitted verbatim for exactly that reason: wrap_words
normalises runs of spaces by design.

That also gives the owner one CONNECT block on both surfaces, which is what
cycle-18 item 4 asked for.

26 tests.
@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit 1c6ac26

… in code

1. gpu_mem hardcoded card0..card2. The cards are read from /sys/class/drm now:
   the node number depends on probe order, so a machine whose card0 reports no
   VRAM (this appliance's layout: the display controller is card0, the APU is
   card1) would have shown nothing. Tested against a fake DRM tree with an
   empty card0 and a populated card1, plus a directory with no card at all.

2. log_lines built the wrapped form of every buffered record each frame. It now
   walks BACKWARDS and stops when the pane is full, so drawing the log costs
   what is on screen instead of 400 records' wrapped length - a record with no
   spaces costs its own bytes and nothing more.

3. 1bit-console reported the fallback with no reason. It now says the status on
   tty1 AND appends it to /data/logs/console-tui.log, so a fallback can be named
   afterwards; the TUI's own stderr already carries the reason (a terminal it
   cannot drive, raw mode refused). All three cases re-measured: TUI exits 0,
   exits 7, absent - the menu always follows and the log line matches.

27 tests, clippy 0 warnings, copyright --check rc 0.
@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit 34e994e

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at 34e994e8c — the four things you asked me to check, checked by running the binary rather than reading it. Two nits, neither a gate.

Built and tested here (cargo 1.98.0, musl target present): cargo test → 27/27 passed, including the parsers against the board's own HRX0: AMD Radeon 8060S Graphics (Node 1) (gfx1151) (122880 MiB, 74158 MiB free) sample, env0_from_the_shipped_file, gpu_mem_reads_whichever_card_reports_vram and both render tests. build-static.sh → build-static: ok 7e30ec558a3d908c 1488472 bytes; the artifact is 0 INTERP entries, ldd says statically linked, ELF 64-bit LSB pie. (1,488,472 B vs the body's 1,465,880 — a different rustc, not a discrepancy in kind.)

1. The two renders. Rendered through the CLI, not just the test: --snapshot 80x25 --fixture and --snapshot 240x67 --fixture are byte-identical to tests/snapshots/80x25.txt and 240x67.txt (cmp clean, 25 and 67 lines).

2. The CONNECT block. On screen at both sizes, in the dialog menu's numbering:

 CONNECT -----------------------------------------------------------------------
  web UI http://192.168.50.110:13305
  api  http://192.168.50.110:13305/api/v1   (API key: press 1)
  ssh  root@192.168.50.110   (your key in /data/ssh/authorized_keys)
  app  Lemonade Mobile: server http://192.168.50.110:13305 + that key

That is inside the first 6 lines at both sizes, which matters because cycle 18's U2 menu connect row (os/v3-c18, vm-check.sh:459-464) reads 1bit-tty1 1 | head -10 and greps exactly CONNECT, web UI http://, api http://, ssh root@, Lemonade Mobile plus web UI http://<v4>:13305 — all five present here, with the two spaces api /ssh the row's grep -F needs.

3. The dialog-menu fallback. Measured on a copy of 1bit-console with its absolute paths repointed (fake pfix=ram,nox cmdline, stub TUI, stub menu), all three cases:

TUI exits 0   -> STUB MENU ran   tty1: "1bit-console-tui exited (0) - starting the dialog menu"   log: exited (0) at 2026-11-03T14:22:04-04:00
TUI exits 7   -> STUB MENU ran   tty1: "…exited (7)…"                                              log: exited (7) at …
TUI absent    -> STUB MENU ran   (no new line, as it should: nothing to fall back from)

So exec on every path, and the reason is named afterwards in both places. rc=$? is taken before the printf, so the status is the TUI's, not the printf's.

4. The probes use Lemonade's environment. facts::lemonade_env() shells . /etc/1bit/libhsa.env and exports IREE_HAL_AMDGPU_LIBHSA_PATH from /etc/1bit/libhsa, then env -0 — the shipped file is the source of truth, not a re-implementation. The GPU probe then runs llama-server --list-devices with env_clear() plus that map (src/facts.rs:461-472), with a PATH fallback only if the env had none. Lemonade up-ness is TcpStream::connect_timeout to 127.0.0.1:13305 (:325-338); the netstat listing is stored separately and reachable() does not use it, which is the cycle-13 "said not listening while curl answered 200" case. Ran --status here on a box with no /etc/1bit, no llama-server: it degrades honestly (no llama-server at /opt/1bit-llama/llama-server, API key: none in /etc/1bit/lemond.conf, DATA not mounted), rc 0, and found the real lemond pid (4606, /usr/bin/lemond) by exact comm match.

Also checked: 1bit-console-menu's diff is only the --action N arm, which calls the same run_item with NONINT left 0 so its dialogs work — no second copy of an interactive action; container-build.sh has all four CONSOLE_TUI_BIN branches including the post-pack -rwxr-xr-x assertion (:477-482); tools/copyright.py --check rc 0, with the snapshot fixtures exempted and the reason written where the exemption is.

Nits, not gates:

  1. "In the dialog menu's own wording" is true of the four greppable lines and not of the rest: cycle 18's block puts the hostname on the CONNECT line (CONNECT <host> web UI …) and says (key: menu item 1) where the TUI says (API key: press 1). The row passes on either surface; one clause in the body would keep a future reader from expecting byte-equality.
  2. The U2 menu connect row is only exercised on a boot — the Rust test asserts the strings are in the frame, which is the right proxy, but the head -10 and the VGA-buffer read are the parts a c18 boot will actually decide.

CI at this head: pr_agent, Analyze×4, CodeQL green, hw_gate SKIPPED, build still in flight (this PR does not touch it, but I am not labelling past a pending check unless you say so).

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit b7d04ec

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

#921 ready for review at b7d04ecb — the full-screen console TUI for tty1 (cycle-18 items 7 + 4), and it
embeds with one variable: build-static.sh prints a musl-static binary, CONSOLE_TUI_BIN=<that> build.sh
installs it; unset leaves the dialog menu in place, so the PR can land without turning anything on.

Three things a reviewer may want to check first:

  • U2 menu connect (pi-os3's os/v3-c18) is honoured from this side. The TUI renders the dialog menu's
    CONNECT block verbatim (CONNECT, web UI http://, api http://, ssh root@, Lemonade Mobile, same
    double spaces) and a test renders 240x67 + 80x25 and asserts all five strings plus the address regex — that row
    cannot go red whichever surface tty1 gets. Test-merged os/v3-c18 into this branch: no conflict (measured,
    then aborted).
  • The nine action labels are unchanged (Show the API key, Reboot / power off) and keys 2-7 hand off to
    1bit-console-menu --action N — the same code U2 menu actions/--selftest already exercise — so the TUI
    cannot drift from the menu. --status/--run/--selftest are untouched.
  • The GPU panel asks the runtime with Lemonade's environment (/etc/1bit/libhsa.env sourced, not
    re-implemented), i.e. not the cycle-13 "no HRX device on a box serving 303 tok/s" bug again. The two probe
    parsers and the two renders are tested against recorded samples; cargo test 27/27, clippy 0, --check 0.

build is the only check still running. No image, no stick, no device from this lane.

@bong-water-water-bong bong-water-water-bong added the tprs-ready TPRS: review verdict READY posted by the standing reviewer; merge gate satisfied label Nov 3, 2026

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at b7d04ecbd — head moved by one commit and the commit is what it says: src/main.rs loses a single line.

     let mut app = App::new(Facts::collect());
-    let mut _msg_clock = Instant::now();
     let mut last_spin = Instant::now();

_msg_clock was never read (that is why it is underscore-prefixed and why cargo did not warn), so this cannot change
behaviour. Re-ran at this head rather than assuming: 27/27 cargo test, --snapshot 80x25 --fixture and 240x67 --fixture
both byte-identical to the committed fixtures, tools/copyright.py --check rc 0. The other 13 files of the PR are
blob-identical to 34e994e8c, the head I verdicted in full (CONNECT block on screen at both sizes, fallback measured for
TUI exits 0 / 7 / absent, GPU probe through lemonade_env() + llama-server --list-devices, Lemonade up-ness by TCP
connect). CI build=SUCCESS.

Label stays tprs-ready; nothing of mine holds it.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at 1a5dc31bf — Merge branch 'main' into os/v3-console-tui, and all 14 files are blob-identical to b7d04ecbd, the head I re-ran in full (27/27 cargo test, both render snapshots byte-identical to the committed fixtures, build-static.sh musl-static with 0 INTERP, CONNECT block on screen at 80×25 and 240×67, fallback measured for exits 0 / 7 / absent, probes through lemonade_env()). Nothing to re-derive; label stays tprs-ready.

One cross-PR note while it is fresh: the CONNECT block here renders api http:// and ssh root@ with two spaces, on purpose, because cycle 18's row was written against the dialog menu's spacing. #925 changes that row's word list to single-spaced and has a CHANGES from me on it — once that row is fixed (two-space words, and a missing- line actually failing the row), whichever surface tty1 gets is checked.

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit 1a5dc31

@bong-water-water-bong
bong-water-water-bong merged commit 25ca34f into main Nov 3, 2026
9 checks passed
@bong-water-water-bong
bong-water-water-bong deleted the os/v3-console-tui branch November 3, 2026 18:54
bong-water-water-bong pushed a commit that referenced this pull request Nov 3, 2026
…nderers draw that screen now

#921 merged the ratatui console TUI, and /etc/inittab's tty1 is the TUI when the image carries the binary and the
dialog menu when it does not (the documented fallback). They draw the same facts and differ in one way that matters
to a grep: dialog collapses runs of spaces in a --menu prompt, ratatui does not, so the TUI's screen says
'api  http://…' and 'ssh  root@…' with two spaces while the dialog menu's says one. U3's sibling row matches those
strings from tty1's VGA buffer, so it would have printed two misleading missing-* words on a TUI image (and passed
only because its marker keys on the single-spaced 'web UI http://' line).

Measured on both renders of the same fixtures: 80x25 TUI snapshot and the dialog menu at 80x25.
bong-water-water-bong added a commit that referenced this pull request Nov 3, 2026
…busy), and no request URL carries a user value (#926)

* os/v3/puppy: the chat page retries the NPU's own refusal (503 device_busy), and no request URL carries a user value

Two follow-ups to #925, both in the page the appliance serves.

1. engine#924 bounds the wait for the one device: a request that cannot have it within --queue-ms is REFUSED with a
   status instead of parked silently, and Lemonade's proxy does not retry that on the request path - so the page is
   the only layer that can. It now retries after the advertised delay (Retry-After, clamped to 10 s, 1 s when the
   header is absent) up to RETRIES = 3, saying 'NPU busy, retrying (2/3) in 1 s' while it waits, and after the last
   attempt it stops with the engine's own words instead of hanging or reporting zero chunks.

   Both shapes #924 can produce are handled, because this page streams: a 503 + Retry-After + {'error':{'type':
   'device_busy'}} (status and headers reach the page through 1bit-webapp-proxy, which already forwards them), and -
   what the page actually sees for a streaming request - ONE 'data: {"error":{...}}' frame before 'data: [DONE]',
   which carries no type at all, so an error frame is retried when it says busy or when the stream had not produced
   a single chunk yet. A stream that fails after it produced content is reported, not silently requested again.

2. CodeQL, on the #925 diff: '?api=' made a fetch URL depend on a query parameter. It was a cycle-15 debug
   affordance nothing in the image uses (the launcher opens http://localhost:8080/ with no query string) and a way
   to make the appliance's own browser POST at another host, so it is gone: the paths are literal same-origin
   '/api/v1/...' and no tainted value reaches a request. ?model= / ?think= / the one-shot ?key= stay (none becomes
   a URL), and the key itself has not travelled in a URL since cycle 17.

Proven in a real browser against the two documented refusal shapes (~/wt/os3i/c18-page-probe.py, screenshots in
/tmp/busy-*): two streaming refusals then the answer; one 503 then the answer (status line reads 'NPU busy,
retrying (1/3) in 1 s' in both); nine refusals -> four attempts then 'NPU busy: still refused after 4 attempts (the
NPU is busy with another request) - press Send again in a moment'; and no refusals -> the answer unchanged. The probe
also caught the bug that would have shipped: the predicate was named busy and var busy=false overwrote it, so the
page answered 'TypeError: busy is not a function'.

U3 webapp page now requires device_busy and 'NPU busy, retrying' in the page the RUNNING proxy serves, printing
busy=N retry-text=N beside the other four counts.

* os/v3/puppy: the CONNECT row reads tty1 through tr -s, because two renderers draw that screen now

#921 merged the ratatui console TUI, and /etc/inittab's tty1 is the TUI when the image carries the binary and the
dialog menu when it does not (the documented fallback). They draw the same facts and differ in one way that matters
to a grep: dialog collapses runs of spaces in a --menu prompt, ratatui does not, so the TUI's screen says
'api  http://…' and 'ssh  root@…' with two spaces while the dialog menu's says one. U3's sibling row matches those
strings from tty1's VGA buffer, so it would have printed two misleading missing-* words on a TUI image (and passed
only because its marker keys on the single-spaced 'web UI http://' line).

Measured on both renders of the same fixtures: 80x25 TUI snapshot and the dialog menu at 80x25.

* os/v3/puppy: a refusal is retried only when the stream produced nothing, so a partial answer is never replaced

Copilot's review of #926, and it is right: the retry condition was `isBusy(j) || /busy/i.test(why) || !chunks`, so
(i) a busy-typed error after content was retried, and (ii) a *reasoning-only* stream (chunks = 0, rchunks > 0) also
passed `!chunks` - and engine#924's own streaming refusal is exactly a reasoning-less-but-something shape. A retry
clears the turn's two output elements, so either case silently replaced text the reader already had.

Now a refusal is retried only when the stream produced NOTHING (no answer chunk AND no reasoning chunk): that keeps
the c17 '0 chunks' recovery, which is what the retry exists for. A failure after any output is reported with the
engine's own words and the chunk count, and the partial answer stays on screen.

---------

Co-authored-by: pi-os3 <pi-os3@1bit.local>
bong-water-water-bong added a commit that referenced this pull request Nov 3, 2026
…s TUI can actually reach an image (#927)

* os/v3/puppy: build.sh hands the console TUI to the container, so #921's TUI can actually reach an image

#921 merged the ratatui console TUI and taught container-build.sh to install it from CONSOLE_TUI_BIN, but nothing
forwarded that variable: docker passes only what `-e` names and the binary - which lives outside the mounted tree,
because build-static.sh builds it on the host - was never mounted. So the container saw the empty string and took its
own documented fallback, in silence.

Measured on cycle 19's first build: `CONSOLE_TUI_BIN=…/1bit-console-tui` on the host command line, and the build log
says `note: CONSOLE_TUI_BIN is not set - tty1 keeps the dialog menu`. The image was the dialog-menu one, which is the
safe failure but not what was asked for, and the same class as the cycle-14 PAYLOAD bug (a host variable that never
crossed into the container).

Now build.sh resolves the path (and refuses a missing file here, where the message can name it), mounts it read-only
at /console-tui and passes CONSOLE_TUI_BIN=/console-tui, printing the binary's sha256 in its first lines so the log
ties the image to a specific build. container-build.sh's own checks are unchanged and still do the work: it refuses a
binary that does not run here, and refuses an image that was given one and has no /usr/local/bin/1bit-console-tui.

* os/v3/puppy: a console TUI without a payload is refused, instead of being silently left out

Copilot's review of #927, and it is right: container-build.sh installs the TUI and verifies it in the packed SFS
inside its `PAYLOAD` block (the TUI belongs to the embedded appliance, not to a bare Puppy rootfs), so a build given
CONSOLE_TUI_BIN and no PAYLOAD would pass the forwarding fixed in the previous commit and then skip the install with
no note - the very failure that commit exists to end, one layer deeper.

build.sh now refuses that combination before the image pull, naming the reason.

---------

Co-authored-by: pi-os3 <pi-os3@1bit.local>
bong-water-water-bong pushed a commit that referenced this pull request Nov 4, 2026
… action

Owner, cycle 20: "can we place the menu above the scrolling logs and below the technical information".

Before, the nine actions were packed two-to-five per row BELOW the log pane, so the pane pushed the menu
off the bottom at 80x25 (4 packed rows, the last one a lone "9 Reboot / power off") and the owner had to
read the log to find the menu. The 80x25 layout also spent nine rows on panels, which is what made the log
pane two rows tall.

Now, top to bottom: header, CONNECT, the status facts, MENU (a rule plus nine numbered rows, never
abbreviated), LOGS (scrolling), one footer line. 80x25 keeps the same six facts on two compact rows (fields
are packed whole, so "DATA" cannot end up alone at the end of a row) and leaves the log pane five rows
(the fixture fills all 25 console rows; the earlier code counted the log title twice and left row 25 blank).

Nothing about the API key changes here. Item 1 keeps ONE label in both key modes - "Show the API key" -
because the appliance ships blank (cycle-20 item 7) and vm-check's `U2 menu` row greps that string off
tty1: a mode-dependent label would turn that row, and pi-release's c15 menugate, red on every shipped
image. The blank-key offer lives where the item-7 work puts it (the CONNECT block and item 1's own
dialog, on os/v3-c20), so no gate edit is needed with this PR. A new test renders a KEYED and a BLANK
box at 240x67 and 80x25 and requires both gate strings on both, which is the regression this PR first
introduced and now holds down.

Measured:
- 80x25 (pty, musl binary): header 1, CONNECT 2-6, compact facts 7-8, MENU rule 9, the nine rows 10-18,
  LOGS title 19, five log lines 20-24, footer 25 - every row used, no blank row.
- 240x67 (pty): MENU rule 17, the nine rows 18-26, log pane 40 rows (was: menu on rows 65-66 under it).
- cargo test 31/31 (5 new: menu between the facts and the log, the nine rows on consecutive lines above the
  log, log pane >= 3 rows at 80x25 and reaching the footer, compact status keeps all six panels and never
  splits a field, item 1's gate string in both key modes), cargo clippy --all-targets clean,
  tools/copyright.py --check rc 0, both fixtures re-rendered (keyed board: unchanged bytes from #921).
bong-water-water-bong added a commit that referenced this pull request Nov 4, 2026
… action (#935)

Owner, cycle 20: "can we place the menu above the scrolling logs and below the technical information".

Before, the nine actions were packed two-to-five per row BELOW the log pane, so the pane pushed the menu
off the bottom at 80x25 (4 packed rows, the last one a lone "9 Reboot / power off") and the owner had to
read the log to find the menu. The 80x25 layout also spent nine rows on panels, which is what made the log
pane two rows tall.

Now, top to bottom: header, CONNECT, the status facts, MENU (a rule plus nine numbered rows, never
abbreviated), LOGS (scrolling), one footer line. 80x25 keeps the same six facts on two compact rows (fields
are packed whole, so "DATA" cannot end up alone at the end of a row) and leaves the log pane five rows
(the fixture fills all 25 console rows; the earlier code counted the log title twice and left row 25 blank).

Nothing about the API key changes here. Item 1 keeps ONE label in both key modes - "Show the API key" -
because the appliance ships blank (cycle-20 item 7) and vm-check's `U2 menu` row greps that string off
tty1: a mode-dependent label would turn that row, and pi-release's c15 menugate, red on every shipped
image. The blank-key offer lives where the item-7 work puts it (the CONNECT block and item 1's own
dialog, on os/v3-c20), so no gate edit is needed with this PR. A new test renders a KEYED and a BLANK
box at 240x67 and 80x25 and requires both gate strings on both, which is the regression this PR first
introduced and now holds down.

Measured:
- 80x25 (pty, musl binary): header 1, CONNECT 2-6, compact facts 7-8, MENU rule 9, the nine rows 10-18,
  LOGS title 19, five log lines 20-24, footer 25 - every row used, no blank row.
- 240x67 (pty): MENU rule 17, the nine rows 18-26, log pane 40 rows (was: menu on rows 65-66 under it).
- cargo test 31/31 (5 new: menu between the facts and the log, the nine rows on consecutive lines above the
  log, log pane >= 3 rows at 80x25 and reaching the footer, compact status keeps all six panels and never
  splits a field, item 1's gate string in both key modes), cargo clippy --all-targets clean,
  tools/copyright.py --check rc 0, both fixtures re-rendered (keyed board: unchanged bytes from #921).

Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Review effort 4/5 tprs-ready TPRS: review verdict READY posted by the standing reviewer; merge gate satisfied

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant