Files
MeshTerm/tests
Jean-Pierre MartineauandClaude Opus 5 ea68b9f890 Navigation: the stack is the model, and ^W leaves it all at once
Navigation was almost a stack already, and the survey in docs/survey-navigation.md
catalogues where it wasn't. This is the machinery for the strict version, plus
the one shortcut past it.

A screen that owns a loop can now keep itself pushed for the whole visit:

    async with session.stay(screen) as visit:
        while True:
            chosen = await visit.result()

One push, one pop, and the *object* survives its sub-screens — so the cursor,
the sort, the scroll offset and any live filter are simply still there on the
way back, rather than being approximated by a `default=` restore that can only
recover the cursor and only when its row still exists. It also retires the
pop-then-re-push-as-a-backdrop workaround: a screen that never left the stack
already is one.

^W unwinds every frame at once, back to the main menu. It resolves the top
screen with POP_ALL, which each navigation boundary turns into PopToMenu, and
every frame pops itself in a finally on the way out — the same single-pop
discipline applied N times. PopToMenu derives from BaseException so it crosses
the `except Exception` guards that stop a broken tool from taking the menu down;
those must not report an unwind as a tool failure. The flag it sets matters
independently of the future: pressed during a device read there is nothing
armed to carry the sentinel, so the next screen the flow tries to open refuses
to open instead.

It declines to fire over anything modal — an unanswered dialog, a progress bar,
a busy splash — which is what `modal` is for. `floating` is only about being
drawn as a box; a select list floats and is not modal, the busy splash is modal
and does not float.

^Q quits from anywhere. Both chords are deliberately unadvertised: a global verb
has no screen to belong to, so a footer atom or an F-key chip would ride on
every screen, and the lane only has three free slots to begin with. A test
walks every string literal in the package to keep them out of the UI.

Quitting mid-screen used to abandon the coroutine driving the app: the chord
exits the application straight from the key handler, so `run_async` returned
while main() was still parked on a screen, and its finally — which closes the
history and chat runs, stops the services and arms the exit watchdog — never
ran. It is cancelled and awaited now.

The menu is the root: built once, kept for the session, and the one frame that
catches the unwind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 13:20:12 -04:00
..
2026-08-30 02:59:01 -04:00