Conversation
New optional full-screen TUI (lazygit-style) as a sibling front-end to the go wizard, per docs/plans/tui-plan.md. Phase 1 ships the entry point and a read-only status dashboard: - `plate tui` subcommand: lazy-import wrapper (commands/tui_cmd.py) over bambu_cli/tui/; in LOCAL_COMMANDS so it can render its own guidance when unconfigured. Interactive-only contract mirrors go: --json emits the error envelope (exit 5, failed_step parse, schema docs/schemas/tui.json) and non-TTY stdin is refused the same way. - DashboardScreen with status + AMS panels fed by StatusService (blocking status() calls in a thread worker); r refreshes, q quits; unreachable printer renders an inline error state instead of crashing. - Textual ships as the optional `tui` extra, pinned >=0.86,<2.0: 8.x breaks pilot text extraction in run_test() (3 dashboard tests fail on 8.2.8), so the cap stays at the tested 1.x line — see the pyproject comment. pytest-asyncio (asyncio_mode=auto) drives the pilot tests. - styles.tcss ships in the wheel (package-data + MANIFEST.in), verified by package_contents_smoke plus a manual wheel listing. Gates: ruff check/format, mypy (71 files), bandit, full suite 1157 passed at 86.21% coverage (floor 83), ci_workflow/syntax/cli_help/privacy/ python_compat smokes, uv build + package_contents_smoke all green.
Extract the wizard's decision logic out of interactive/session.py into a new front-end-agnostic interactive/core.py, then build the TUI prepare flow (source -> presets -> slice -> preview) on top of it. Extraction (session.py 750 -> ~330 lines, behavior byte-identical; test_interactive_session.py and test_wizard_guided.py pass unmodified): validate_source, material/quality choices + guidance, AMS detection (incl. the single-arg detector fallback), build_job_namespace, run_prepare_pipeline, preview_rows (preserves the 11-char label pad), slicer preflight checks, workdir hygiene helpers, WizardState/GoSteps moved verbatim. session.py aliases the old private names so its module globals and existing imports keep resolving. TUI side: PrepareScreen (source Input with inline validation, material/ quality RadioSets with guidance, supports Checkbox, AMS-detected default with '(detected in AMS)' tag, preview honoring the pre-sliced caveat), PreflightErrorScreen pointing at plate setup, PipelineService driving download -> extract -> slice via the injected GoSteps in a thread worker. Print action stays disabled: no TUI path reaches confirm=True until the Phase 3 modal (adversarially verified). Three bugs found in adversarial review and fixed with guarding tests: - Escaping mid-prepare leaked the temp workdir (worker owned the dir; now the screen creates it up front and on_unmount discards it even when the result never lands; late worker completion is defensive). - Late AMS detection silently overrode a manual material choice made while the MQTT read was in flight; detection now yields once the user has touched the RadioSet (programmatic pre-selection exempt via a sentinel, since Textual delivers Changed asynchronously). - Message-less prepare failures rendered as 'Printer unreachable'; the prepare path now has its own fallback wording. Gates: ruff check/format, mypy (73 files), bandit, full suite 1205 passed at 86.81% coverage (floor 83), syntax/cli_help smokes green; docs test-count snapshots refreshed (drift guard green).
The TUI can now start a print, behind a single explicit gate, and watch it live. Full flow: dashboard -> n -> source/presets -> prepare -> confirm -> monitor -> terminal state, covered end-to-end in a pilot test. - ConfirmModal (Start print / Upload only / Cancel) is the ONLY code path in the TUI that builds a job namespace with confirm=True — one grep-able call site, adversarially verified: double-press, modal bypass, and re-entry attempts all reach the job runner at most once (synchronous in-flight flag set before any await point). - Workdir ownership is one-owner-at-a-time, documented in confirm.py: prepare owns until take_result() hands off to the modal; job success and cancel clean up (cancel first preserves the sliced file with the wizard's own 'Nothing sent. Sliced file kept at ...' message — now a shared core.decline_message the wizard also calls, so wording cannot drift); Esc hands ownership back with the preview intact; job failure keeps the file for retry; quitting with the modal open cleans up. - MonitorScreen + MonitorService poll the status provider until a terminal gcode_state; the terminal set is imported from protocols/mqtt.py (hoisted to TERMINAL_GCODE_STATES, no behavior change) and identity-asserted in tests so it cannot fork from the CLI monitor. Esc detaches without sending anything to the printer. - Quit guard: ctrl+q (the only binding that pierces the modal) is refused while a job upload is in flight; every quit path routes through the one action_quit. - Job worker wraps cmd_job in redirect_stdout/stderr so Rich logging can't draw over the UI; on failure the last lines of captured output are appended to the error (bounded, markup-safe). - Fixed a latent Phase 2 bug: screens stored _closed/_running, which shadow textual.MessagePump internals (the modal's buttons would have been dead); renamed to _left_screen/_job_running and swept the whole package against the Textual base-class attribute set. Adversarial verification round-tripped twice: the first pass confirmed the safety design but caught a vacuous quit-guard test (plain 'q' never reaches the app under a modal — the test passed with the guard deleted); it now presses ctrl+q and fails when the guard is sabotaged. Gates: ruff check/format, mypy (76 files), bandit, full suite 1223 passed at 86.94% coverage (floor 83), syntax/cli_help smokes, no leaked workdirs under -W error::ResourceWarning.
…hardening Final build phase; the TUI is feature-complete and documented. - Help overlay on ? (and F1, because ? is a printable char the source input legitimately swallows on the prepare screen): pure-presentation HelpScreen listing every key, closing on Esc/?/F1/q — q closes the overlay rather than quitting. A drift guard asserts every key the overlay documents is actually bound somewhere. - Footers made honest per screen: app-level bindings are active everywhere but hidden; each screen's Footer advertises only keys that work there (the confirm modal previously advertised nothing). - 80x24 proofs: pilot tests run the full flow at size=(80,24) with deep paths and a ~500-char slicer error, asserting overlay/preview widths stay within the terminal. - Outcome-stranding bug found by adversarial review and fixed at two independently-tested layers: a job finishing while an overlay covered the confirm modal had its dismiss silently rejected (frozen modal, dead buttons, monitor never opening). action_help now refuses while the top screen is mid-job, and _job_done pops anything above the modal before dismissing — either layer alone passes its test. - Docs: manual.md 'Full-screen mode (plate tui)' section (screens, key table, safety model, go-vs-tui positioning), README mention, CHANGELOG Unreleased entry, quality-roadmap coverage + tracking rows. - Coverage hardening (tests/test_tui_polish.py, 34 tests): tui package now 95.7-100% per module; repo total 87.89% (floor 83). Tests also gained an isolated-cwd fixture — the decline path deliberately moves the kept file into cwd, and tests had been littering the repo root. Gates: ruff check/format, mypy (77 files), bandit, full suite 1257 passed at 87.89% coverage, syntax/cli_help/ci_workflow smokes, uv build + package smoke (help.py + styles.tcss in the wheel), privacy smoke zero tracked hits, python_compat smoke, no leaked workdirs.
…he slicer)
The prepare screen gains an opt-in Settings screen ('s' / a button)
exposing the CLI's entire slicing surface — requested after the live
test. No new slicing machinery: everything rides the existing
--set/--set-filament plumbing and named slice flags.
- Grouped form (Quality / Strength / Supports / Adhesion / Filament /
Speed / Plate) of 25 fields, each mapping 1:1 onto a real slice
parser dest — pinned by a test that introspects build_parser(), and
choice fields (seam position, ironing) derive their option sets from
the parser so they can never drift from what the CLI accepts. Blank
field = profile default, exactly like leaving the flag off.
- Searchable browser over every setting in the installed OrcaSlicer
profiles, via a new slicer.options.setting_catalog seam that
slice --list-settings now also reads through (one discovery path,
output unchanged). The override bucket comes from the profile the
key was found in, which is what routes filament_flow_ratio to the
filament profile instead of silently no-oping as a process key —
proven by a read-back test that runs the real cmd_slice against the
hermetic orca stub and asserts the values inside the temp profiles
the stub received. With no readable profiles (e.g. --sim) the
browser degrades to free-form KEY=VALUE entry with an explicit
filament:/process: prefix for routing.
- Safety unchanged: overrides go through the same
_validate_slice_options bounds the CLI applies (nozzle 999 refused
inline, from the form and from the browser path), and the slice-time
validation remains the backstop.
- Wizard untouched: empty overrides return the identical namespace
object (sabotage-tested), and plate go's tests pass unmodified.
- Adversarial review caught two things fixed here: the s keybinding
bypassed the disabled Settings button on pre-sliced sources
(discarding a ready-to-print workdir for settings that could never
apply — now both doors share one settings_lock_reason), and the
seam-position hint suggested a value the CLI parser would reject.
- Changing settings after a preview discards the stale workdir and
requires re-preparing; overrides survive the confirm round-trip.
Gates: ruff check/format, mypy (79 files), bandit, full suite 1296
passed at 88.35% coverage (floor 83), syntax/cli_help/ci_workflow
smokes, docs-consistency green with refreshed counts.
`class SettingsScreen(Screen[SliceOverrides | None])` is a PEP 604 union in a RUNTIME position: a class base is evaluated eagerly, so `from __future__ import annotations` does not defer it, and `type | None` raises TypeError before 3.10. The 3.9 CI leg died at collection (1262 collected / 1 error); the other five legs passed, Windows included. Now `Screen[Optional[SliceOverrides]]`. python_compat_smoke.py could not have caught this, for two independent reasons: it returns early on any module carrying `from __future__ import annotations` (nearly all of them), and it only ever walked annotation nodes, never class bases. Adds `_check_pep604_runtime` with two rules picked to have no false positives on real bitwise code — any `|` inside a class base, and any `X | None` outside an annotation. Both sabotage-verified: reverting the fix fails the smoke; the seven legitimate `os.O_CREAT | os.O_EXCL` / set-union expressions in the package stay silent. The remaining gap (a runtime union of two non-None types outside a class base) is documented in the docstring rather than implied to be covered. Verified on a real CPython 3.9.25 interpreter, not by inference: every bambu_cli module imports, and the full suite is 1296 passed / 88.38% coverage.
Documentation pass over every tracked doc. Three corrections matter beyond refreshing numbers: SECURITY.md described `--confirm` as the gate for physical actions. That is true of the non-interactive commands and NOT of `plate go` / `plate tui`, where the user never types the flag and the gate is a confirmation dialog. Verified before writing: both front-ends refuse `--json` and a non-TTY stdin with exit 5 (session.py:343-356, tui/entry.py), and each has exactly one `confirm=True` call site (session.py:291, tui/screens/confirm.py:108), so the property the flag protects still holds -- only its form differs. AGENTS.md had a paragraph sitting between two rows of the quality-gates table, which splits it in half wherever markdown is rendered; AGENTS.md ships in the sdist, so that was visible on PyPI. Moved below the table. The module table had also drifted seven entries behind the package: added `interactive/`, `tui/`, `tlspin.py`, `netsafety.py`, `printables.py`, `ams.py`, `utils.py`, and marked `go`/`tui` as human-only surfaces with no machine contract. CONTRIBUTING.md listed four of the eight smokes CI runs, so a contributor could run everything the doc named and still go red -- which is exactly how the 3.9 break reached CI. All eight are listed now, with the PEP 604 runtime-position trap written out, and the PR template gained the compat smoke as a checkbox. Also: troubleshooting gained the three `plate tui` symptoms (missing extra, interactive-only refusal, empty settings browser + the `filament:` prefix that avoids a silent no-op), keyed to the strings the command actually prints; docs/ index now lists troubleshooting.md and plans/; the TUI plan is marked implemented rather than "draft"; releasing.md verifies the `[tui]` extra resolves, since a broken optional extra is invisible to the default install path; live-printer-smoke records that the interactive front-ends are hand-tested only. Numbers refreshed from measurement, not inference: 1296 passed / 1297 collected, 88.35% local Linux, and CI run 30632442521 giving Windows 88.09% / Linux 88.51% / macOS 88.33%. Windows is still the binding leg. Schema count corrected 25 -> 26 (tui.json). Added ratchet headroom to the roadmap: 85 is supported by this data, 88 is not (Windows clears it by 0.09pt).
The advanced-settings screen asked the user to hand-edit an override string:
select a key, get `key=value` dropped into one text box, edit around the `=`,
and remember a `filament:` prefix for the degraded case. Four of the named
flags with closed option sets (wall type, support type, seam position, ironing)
were free-text boxes you could typo into.
Now the shape of the control is derived from data. New
`setting_value_domains()` in slicer/options.py collects every distinct value a
key takes across the installed profiles -- deliberately separate from
`_known_setting_keys`, whose `{key: representative_value}` contract feeds
`slice --list-settings` and must not change shape. `editor_for()` turns that
domain into a control: `0`/`1` is a toggle (checked before "numeric", or every
OrcaSlicer toggle renders as a number box), all-numeric is a number box, a short
set of short strings is a dropdown, everything else stays free text. Picking a
key now fills its name, shows the profile's own value, pins the bucket to the
profile the key came from, and seeds the editor. Pending overrides are a list
you can click to edit or remove individually.
The inference is a shortcut, never a cage -- three things that fell out of
testing and matter more than the widgets:
* Observed values are a *subset* of what OrcaSlicer accepts. Profiles that only
ever say `grid` must not make `gyroid` unreachable, so every dropdown carries
a "type a custom value" entry. Caught by a test that tried to set a legal
value the local profiles happen not to use.
* The bucket picker was sticky: routing one key to filament made the *next*
unclassifiable key inherit filament, which is the silent no-op this split
exists to prevent, only in reverse. It now resets to process per key.
* An untouched dropdown was recorded as `key=`, sending an empty override. Blank
in a dropdown means "not chosen"; it is refused. An empty *text* value is
still allowed, since clearing a setting is legitimate.
All four guards sabotage-verified (each test fails with the guard removed),
including the async-`Input.Changed` no-op guard -- whose first test was VACUOUS:
it used a number-valued key, where reconfiguring does not touch the value. Only
a dropdown key discriminates, because re-running `set_options` is what resets a
Select. Same vacuous-test trap as the Phase 3 quit guard.
`parse_override_entry` is deleted with its tests -- the screen was its only
caller and there is no string syntax left to parse.
1307 passed / 88.53% (up from 88.35%); tui/screens/settings.py 99.0% with zero
uncovered statements, tui/settings_model.py 100%. Manual, troubleshooting (the
`filament:` prefix entry written this morning is now wrong and was rewritten),
CHANGELOG and the measured roadmap/backlog numbers all updated.
Phase 4 proved every other screen at the smallest supported terminal but not this one, and the editor just gained a bucket dropdown, three value controls and a second list. Asserts the widgets stay within 80 columns and that an override can still be added and applied there — rendered is not the same as usable.
…ating tags Rendered the real screen to SVG and looked at it. Four things were wrong that no assertion was going to catch. **The bucket tag was invisible.** OptionList prompts passed as `str` are parsed as Rich markup, so "[filament] filament_flow_ratio = 0.98" rendered as " filament_flow_ratio = 0.98" -- the process/filament fact the browser exists to show, silently eaten. Tests could not see it because the prompt *string* is intact; only the render differs. Both lists now pass `Text`, which also protects any bracketed profile value (list-valued settings render as "[0.98]"). Guarded by a test asserting the prompt type, since asserting the string cannot fail. **Every screen claimed to be the printer dashboard.** `Header` watches `screen.sub_title` and only the App set one. Settings, prepare and monitor now name themselves. **The form was unreadable.** A stacked label over a default (bordered, 3-row) input meant a 30-row terminal showed five of 25 fields, and four at 80x24. Label and control now share one flat row -- 17 fields at 100x30, three whole groups at 80x24. Select needed its inner SelectCurrent flattened too (own border, background and padding) or dropdown rows broke the rhythm. **Dead space in the editor.** The profile-value note reserved its padding while empty, leaving a gap mid-editor; it now collapses. The value control moved into the same one-row layout as the rows above it, and the pending list shrank, so the whole editor plus both button rows fits on one 80x24 screen. Also gave the eleven fields that had no hint an example value -- an empty value column read as an inert field rather than an empty one. 1309 passed / 88.54%; settings.py 99.1%, settings_model.py 100%. ruff, format, mypy, bandit and the smokes all green.
Review pass against the real stock Bambu profiles, not a fixture. The toggle
inference required only that every observed value be in {0, 1}, so a key seen
holding "0" in every installed profile rendered as a switch. Measured: 35 keys
were inferred as toggles and only 6 ever actually varied between 0 and 1.
The other 29 are numbers sitting at zero, and the switch made every value except
0 and 1 unreachable -- with no custom escape, since only the dropdown has one.
Real casualties: raft_layers and skirt_loops and skirt_height (counts),
support_filament and support_interface_filament (AMS slot indices, so you could
not select slot 3), bottom_shell_thickness, max_bridge_length,
max_travel_detour_distance, support_expansion. This is the same "picker as cage"
bug fixed for dropdowns one commit ago, missed for switches.
A toggle now requires BOTH states to have been observed. That leaves exactly the
6 keys that genuinely vary -- enable_support, filament_is_support,
filament_soluble, activate_air_filtration, reduce_fan_stop_start_freq,
support_interface_loop_pattern -- and moves the rest to a free number box.
Sabotage-verified: restoring the old subset rule fails both new tests.
Also locks the "Applies to" dropdown for a key found in the profiles. The bucket
is decided by `bucket_for_key` from the source profile, so the control was
accepting a choice and silently discarding it. It reports the fact now, and
unlocks for a key nothing can classify, where the user's choice is the only
signal there is.
Measured on the real profiles (641 files, 176 keys): catalog loads in 12 ms cold
and 0.1 ms warm, so reading the profiles a second time for value domains costs
nothing at mount. Editor split is now 125 number / 36 select / 6 switch / 9 text.
1312 passed / 88.55%; settings.py 99.1%, settings_model.py 100%. ruff, format,
mypy, bandit, all eight smokes and package_contents green.
Rendered all six screens and looked at them as a set. The settings screen had
been through this; the rest never had.
**Modals looked broken.** `border: thick $primary` renders as solid blue slabs
across the top and bottom, so the help overlay and the confirmation dialog read
as damaged widgets rather than frames. Both are `round` now, matching every
other panel, and both carry a border title.
**The confirmation dialog showed a temp path and nothing else.** It is the one
screen in the app that starts a physical action, and it could not tell you what
it was about to print. It now renders the preview rows the prepare screen has
already computed -- model, printer, material and quality, time and filament
estimate -- above the path. No new computation: `PrepareResult.rows` simply
travels to the modal.
**The dashboard was two unlabelled boxes stretched to full height**, each mostly
void, with the content pinned to the top. The panels are titled ("Printer",
"AMS") and hug their content now.
**A running job had no bar on the dashboard** even though the monitor has had
one all along. `JobProgress` gained a `compact` mode -- bar only -- because the
full table repeats state, progress and layer, which the printer panel is already
showing. It appears for RUNNING and PAUSE only: a 0% bar on an idle printer
reads as a stalled print. `_ACTIVE_STATES` is deliberately not "not terminal",
which would include PREPARE where a percentage means nothing.
Also removed a duplicate `#browser-list` rule left by the settings work, where
the second silently overrode the first.
Both new behaviours are guarded, and the hide-when-idle guard is
sabotage-verified. 1312 passed / 88.58%; every tui widget at 100%. ruff, format,
mypy, bandit, smokes and package_contents green.
Known remaining: the prepare screen wastes its right half -- the form sits in a
narrow left column while ~40% of the width is empty. That needs a two-column
layout, which is a real restructure rather than a style fix.
A str cell in a Rich table is parsed as markup. Every one of these sinks renders data the printer or the user supplies, so a model named "bracket [remix].stl" rendered as "bracket .stl", and one shaped like "a[/b]c.gcode" raised MarkupError in the middle of a render. Wrap the cells in Text() at the three sinks that take outside data: the status panel's label/value rows, the AMS tray table, and the confirm modal's summary. Tests assert the rendered cell, not the source string — the string was always intact; it is the render that dropped the tag.
The settings screen grew a searchable browser over every key in the installed OrcaSlicer profiles, with the editor control inferred per key from the values those profiles happened to hold — a switch for 0/1, a dropdown for a short observed set, otherwise a number or text box. It worked, and it was the wrong thing to ship. The inference is a tuned heuristic over a third-party tool's vocabulary (_MAX_SELECT_CHOICES=12, _MAX_CHOICE_LEN=40, and a 0/1 rule that already needed its own bugfix after caging raft_layers and every other constant-zero count at 0/1). It drifts silently whenever OrcaSlicer ships new profiles, and no test can catch that drift. It also generated most of this branch's fix commits. The user it serves — someone hand-tuning filament_flow_ratio — is a --set user already, and `slice --list-settings` is the honest way to show them the vocabulary. Kept: the 25 named fields (a closed set, the CLI's own flags, no inference) and the KEY=VALUE escape hatch, now a plain key + bucket + value editor. The bucket is the user's choice, exactly as --set vs --set-filament is on the command line, and it resets to process for each new key so a filament choice never carries over into the next override — the silent-no-op this split exists to prevent, in both directions. Also drops the plumbing that existed only to feed the inference: slicer.options.setting_value_domains/_setting_value_domain, and the profiles_dir the prepare screen read purely to hand to the browser. setting_catalog stays; it is what --list-settings reads. Net -829 lines: source -366, tests -463, docs unchanged. Full suite green (1303 passed), coverage 88.4%, and tui/ coverage improved — settings.py 98.4%, every tui/ module at or above 95.8%.
The manual had a section literally titled "`plate go` or `plate tui`?",
and the tui help string ("dashboard, guided print, job monitor") sold the
TUI as a replacement for the wizard. Between them they presented two
products and made picking one the reader's problem.
There is one interactive front door. `plate` walks you through a print;
with the optional extra, `plate tui` additionally gives you a live view of
the printer. Both drive the same pipeline, so nothing is reachable from
one and not the other, and which you get is a question of what your
terminal can do — not a decision to research.
So: the "or?" section becomes "Where it runs", stating the fact (Textual
is never a runtime dependency, so the wizard keeps working on dumb
terminals, slow SSH links and with screen readers) instead of posing the
choice. The tui help and command docstring lead with what the TUI is
uniquely for — the live view — rather than restating what `go` does.
Also drops a stale README claim: it still advertised the advanced-settings
screen as "every setting in your installed profiles, searchable", which
described the browser removed in the previous commit.
Docs and help strings only; no behaviour change.
DLANSAMA
marked this pull request as ready for review
August 5, 2026 12:28
DLANSAMA
added a commit
that referenced
this pull request
Aug 5, 2026
docs/schemas/tui.json arrived on main with the TUI. The drift gate did exactly what it was built to do and failed with "no contract generates: tui.json" — the check works in both directions, so a published schema with no model behind it is a failure, not a silent pass. Same shape as Go: both are human-only front-ends with no machine contract, so the only payload either emits is the --json refusal. Verified the generated schema loses nothing from the hand-written one.
DLANSAMA
added a commit
that referenced
this pull request
Aug 5, 2026
docs/schemas/tui.json arrived on main with the TUI. The drift gate did exactly what it was built to do and failed with "no contract generates: tui.json" — the check works in both directions, so a published schema with no model behind it is a failure, not a silent pass. Same shape as Go: both are human-only front-ends with no machine contract, so the only payload either emits is the --json refusal. Verified the generated schema loses nothing from the hand-written one.
DLANSAMA
added a commit
that referenced
this pull request
Aug 5, 2026
…writing them (#101) * refactor: generate docs/schemas from typed contracts instead of hand-writing them The 25 files in docs/schemas were hand-maintained, which meant nothing stopped them drifting from what the commands actually emit. They are now generated from frozen dataclasses in bambu_cli/contracts/, and CI regenerates and diffs, so drift is a build failure. bambu_cli/contracts/base.py Contract base + spec() field constraints bambu_cli/contracts/models.py 25 published contracts + 8 nested structures scripts/gen_schemas.py the generator (--check is the CI gate) Fidelity was verified before overwriting anything: the generator was diffed against the committed schemas until it produced ZERO losses — every constraint the hand-written files expressed (minLength, minimum, nested required, field descriptions, the whole status.printer shape) is reproduced. The 78 differences that remain are all strictly more precise: types alongside consts, item types on arrays, and explicit nullability. Two real defects surfaced while doing it, both previously invisible: - download.json required 7 fields; a naive model made 4 of them optional. Caught by the loss diff, fixed with spec(required=True). - error_envelope typed next_command as `{}` (anything), which hid that job/send emits it as null. The generated schema is explicit, and the contract test failed until the model said so. Pydantic is a DEV dependency only. It derives JSON Schema from the dataclass annotations at build time and is never imported at runtime — verified by a test that subprocess-imports the package and asserts pydantic is absent from sys.modules, and visible in uv.lock: runtime deps stay at 3, with pydantic under the test extra behind a python_version >= '3.10' marker. Serialization stays in emit_json because that pass applies the credential redaction a model_dump_json() would bypass; emit_json/emit_json_line now accept a contract or a plain dict. The contracts annotate optionals as `X | None`, which only evaluates on 3.10+. Nothing at runtime resolves them (dataclasses keeps annotations as strings), so the 3.9 floor is unaffected — asserted by a test, and the generator refuses to run below 3.10 with an explanatory message rather than failing obscurely. Wired so far: version, light, pause, resume, stop. The remaining commands still emit dicts; every one of them is validated against its generated schema by tests/contracts/, so the contract holds either way. Converting the rest -- especially job's incrementally-built summary -- is follow-up work. Docs: schema counts and coverage refreshed to measured values (1215 collected / 1214 passing, 87.0% Linux) rather than left stale. Gates: 1214 passed (was 1177), coverage 87.0% (was 86.5%), ruff/ruff-format/ mypy/bandit/layers/drift-check/syntax/help/workflow/compat/package smokes green. Verified on a real CPython 3.9.25 that contracts import, payloads render, and pydantic stays absent. * feat: add the Tui output contract now that #97 has landed docs/schemas/tui.json arrived on main with the TUI. The drift gate did exactly what it was built to do and failed with "no contract generates: tui.json" — the check works in both directions, so a published schema with no model behind it is a failure, not a silent pass. Same shape as Go: both are human-only front-ends with no machine contract, so the only payload either emits is the --json refusal. Verified the generated schema loses nothing from the hand-written one. * docs: record the measured numbers at this point in the stack 1374 collected / 1373 passing, 89.0% Linux — measured on this branch after the rebase onto main-with-TUI, not inherited from either side of the doc conflict. These were placeholders until measured: each stacked PR has to stand up green on its own, and test_docs_consistency rejects a placeholder exactly as it rejects a stale number. PR #102 re-measures on top. --------- Co-authored-by: DLANSAMA <258674612+DLANSAMA@users.noreply.github.com>
DLANSAMA
added a commit
that referenced
this pull request
Aug 5, 2026
…#107) * docs: 0.5.0 truth pass — correct what the docs now get wrong Audits every tracked doc against the 0.5.0 code and the release commit's own CI run (31044588411 on 5b08720), and fixes what had gone false or stale. Actively false, now corrected: - CONTRIBUTING claimed the four 2026-07 audit gaps were open. All four had closed: B.4 cli helper extraction, B.5 single TLS pin helper, the remaining JSON schemas (generated now), and the camera bind/pin hardenings. It also cited ~82% coverage against a measured 89. - quality-roadmap said "Phase D schemas largely landed but not complete for every command" while docs/api.md and a contract test say the opposite and enforce it. Schemas are complete and generated. - quality-roadmap still framed the TUI as unmerged work on feat/tui that "has not reached CI yet". It shipped in 0.5.0 (#97 + #104). - test-backlog put the mutation floor at 40 (it is 48) and asked for a mutmut re-run on slicer/output.py that already happened on 2026-08-04 and disproved the prediction it was based on. - manual's OrcaSlicer detection table listed only the legacy Flatpak app id and no Flatpak profile paths at all, contradicting the prose two paragraphs above and the real candidate list in config.py. - docs/plans/interactive-mode-plan.md still read "Draft for implementation" and told the reader not to start until 0.4.0 was tagged; plate go shipped in 0.4.0. Marked implemented, historical text kept as written. Stale or incomplete: - Measured numbers refreshed with their source and date: 1419 passing / 1420 collected, 89.1% branch coverage over 8120 statements on local Linux, and the full CI matrix (Windows 88.8% binding, macOS 89.1%, Linux 89.2-89.3%). Local and CI figures are cited separately, not reconciled. - tui/ coverage row was one module behind (#104 added widgets/summary.py): now 14 of 18 at 100%, package minimum 95.8%. - AGENTS module table was missing bambu.py and printer.py; its repo-only list was missing docs/releasing.md, docs/README.md and docs/plans/*. - AGENTS and CONTRIBUTING gate lists omitted pip-audit, gen_schemas --check and check_layers, all blocking in the same CI job. - releasing.md's dev-bump example still said 0.5.0.dev0. - README told pipx / uv tool users to `pip install 'platecli[tui]'`, which installs into the wrong environment; and its agent section did not mention that go and tui refuse --json. No grade was inflated: the scoreboard stays A- / A, coverage 89 against a target of 92, per-module floors still unenforced, camera residuals still open. * fix: plate tui — stop the scrollbar clipping which material was detected On a terminal short enough that the prepare form overflows, #prepare-inputs grows a vertical scrollbar, and a Textual scrollbar takes real cells rather than overlaying. The longest radio label is the AMS-detected material — "PLA — easy, rigid, most models (detected in AMS)", 49 cells, plus 4 for the toggle and its padding = 53 — against a column that offered 54. One cell of slack, which the scrollbar consumed: the label rendered as "(detected in AMS" with the closing paren shaved off, so the screen silently truncated the one thing that tells you which filament it found. Widening the column to 62 leaves 54 cells even while scrolling. Found by recording the TUI and looking at the frames — every existing assertion passed, because the label *string* was intact and only its rendering was cut. The new test asserts the RadioSet has room for its widest button while the scrollbar is up, and fails when the column goes back to 60. * chore: untrack the raw hero capture, and ignore raw captures going forward docs/job-hero.mp4 is the full-length VHS capture of the hero run — 2.3 MB, most of it the dead air while OrcaSlicer works. The postable cut that scripts/ trim_hero.sh produces from it (docs/job-hero-post.mp4, 427 KB) is already committed and is what the launch material uses, so the raw file was pure repo weight and no doc referenced it. It was never meant to be tracked. It was untracked AND unignored, which the post-audit gameplan explicitly called out as a trap, and it duly got swept into #102 — a test-decomposition PR — by a `git add`. Ignoring it closes the trap. The file stays on disk for re-cutting the video without re-running a real print; it stays in history, which is fine: frames were extracted and checked and it leaks no IP, serial, or home path. This only removes it from HEAD. * docs: record the TUI against a real print, and put each GIF under what it shows The 0.5.0 headline feature had no imagery at all, and the picture below its README section was the `plate doctor` GIF — health-check output captioned as if it illustrated the live dashboard. Meanwhile `plate doctor` is introduced 40 lines earlier with nothing beside it. Both are now under what they actually show: doctor beside the `plate doctor` line, the new capture under "Watch the printer while it works". docs/tui.gif is a real printer mid-print, not a mockup and not --sim: the dashboard at 13% with nozzle 219.9->220C, bed 60->60C and layer 4/240, then the job monitor (including its "the print keeps going; nothing here can stop it" line), then the two-column prepare screen. Frames were extracted and read before committing — no IP, serial, access code or home path in any of them, and the shell prompt is bare. docs/tui.tape records how, and carries the two things that cost time here: vhs Width/Height are PIXELS, not cells, so the obvious 1100px window is only ~93 columns and silently captures the NARROW single-column prepare layout rather than the two-column one; and an idle printer records as a static 0%, so the print has to be running before the tape is rolled. --------- Co-authored-by: DLANSAMA <258674612+DLANSAMA@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
plate tui— a full-screen Textual terminal UI, shipped as an optional extra (pip install 'platecli[tui]').StatusServicethread workers.bambu_cli/interactive/core.pyand shared, sogoandtuicannot drift. Source validation, material/quality presets with the AMS-loaded filament pre-selected, supports, slice + time/filament preview.confirm=Truecall site in the whole TUI; the monitor follows a print to a terminal state.?/F1), per-screen honest footers, 80×24 proofs, docs.son prepare): the namedsliceflags as a grouped form, plus a searchable browser across every setting in the installed OrcaSlicer profiles.It is a front-end over the existing pipeline, not a second implementation.
Settings: picked, not typed
The settings screen originally asked you to hand-edit a
KEY=VALUEstring with afilament:prefix to remember. That is gone. The shape of each control is now derived from the installed profiles: a newsetting_value_domains()collects every distinct value a key takes, andeditor_for()maps that to a toggle (0/1), a dropdown (a short closed set), a number box, or free text. Picking a key fills its name, shows the profile's own value, and pins the process/filament bucket to the profile the key came from.Three things that fell out of testing and matter more than the widgets:
gridmust not makegyroidunreachable, so every dropdown carries a type a custom value entry. The inference is a shortcut, never a cage.processper key.key=, sending an empty override. Blank means "not chosen" and is refused; an empty text value is still allowed, since clearing a setting is legitimate.Visual pass
The screen was rendered to SVG and inspected, which caught four defects no assertion could:
[filament] key = 0.98rendered askey = 0.98. Prompts now passText, which also protects bracketed profile values ([0.98]). Guarded by asserting the prompt type, since asserting the string cannot fail.Headerwatchesscreen.sub_titleand only the App set one. Pre-existing across the whole TUI.Safety
Unchanged from the CLI, and deliberately so:
confirm=Truecall site, guarded by modality plus a synchronous_job_runningflag._validate_slice_optionsthe CLI runs.go:--jsonor a non-TTY stdin exits5with the standard error envelope (schemadocs/schemas/tui.json).plate gois untouched and remains the no-extra-dependency path for SSH, dumb terminals, and screen readers.Gates (measured locally, Linux)
ruff check/ruff format --checkmypy -p bambu_clibandit -llpytest -W error::ResourceWarning -m "not live"tui/screens/settings.py99.1% with zero uncovered statements;tui/settings_model.py100%.Every new guard is sabotage-verified — each test was re-run with the thing it protects removed, and each one failed. That process caught a vacuous test: the async-
Input.Changedguard passed with the guard deleted because it used a number-valued key; only a dropdown key discriminates, since re-runningset_optionsis what resets a Select.Why this is still a draft
The settings screen has not been exercised by a human against a real printer. Phases 1–4 were live-tested; everything from Phase 5 onward has only been driven by pilot tests and inspected as rendered screenshots. That live test happens before this leaves draft.
textualis pinned>=0.86,<2.0on purpose — 8.2.8 makes three dashboard pilot tests fail (run_test()text extraction returns empty); tested green on 1.0.0. Don't widen that pin without re-runningtests/test_tui_dashboard.py.Known accepted quirks
mon an idle printer instantly reports terminal IDLE — faithful tomonitor_status.