Eleven files defined the same _plain (plus test_chat's _strip_ansi and
test_timemachine's renderable flavour), and two of the copies had
already diverged into join-without-strip. tests/conftest.plain is now
the single strip-and-join reader — a list of lines or a bare string —
and every file imports it; the local variants keep their names so call
sites stay put (housekeeping H-003).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"Atlas" named a book of maps — a thing you leaf through — and this screen has
never been that. It holds one node in focus, fans its neighbourhood, and moves
when you press Enter. The trail across the top is a walk, the list heading says
Enter walks, the tests call the helper walk_to. Only the name disagreed.
So the name goes: the tool is `walk` / "Mesh walk", the screen is WalkScreen in
ui/walk_screen.py behind open_walk, and every prose mention follows — the
README feature table, the node-vs-contact boundary in CLAUDE.md, the
windowed-list pattern note that cites this screen as its origin, pathline's
separator example, topology's whole-graph view and its prefix-coalescing
rationale, and the purge-test-data skill that warns about fake nodes showing up
here.
No behaviour moves. The tool keeps its 🌐 icon, its Mesh category and its
order beside Map — the logical shape next to the geographic one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Merging a repeated hop into one marker is right across paths and wrong
within one. A via chain that names the same hop twice closes a cycle, and
the left-to-right flow has nowhere to seat it: the balanced-x relaxation
iterates a cyclic edge set, never reaches its fixed point, and returns
wherever the sweep budget stopped. The loop's relays then land in one
column — markers piled a cell apart, labels colliding or dropped, the back
edge falling as a bare vertical — while the hops outside it are crushed
against the ends. Worse, the residue depends on set iteration order, so
the pile's column swung with the hash seed: the very frame-to-frame jump
the first-appearance node order exists to prevent.
render_path_graph grows allow_duplicate_nodes, which hands each revisit
its own marker (occurrence-qualified ids the caller's callbacks never see,
so both markers draw as the same node). The walk is acyclic again, spreads
evenly, and reads in the order it was heard. Relays shared *between* paths
still merge — that fan is what the widget is for.
Opt-in, because the two situations want opposite things. The Trophy case
scores a path we composed, so collapsing a revisit is deliberate there —
re-walking a node must not make the shape look bigger than the ground it
covered. An overheard chain has no such duty: nothing in it is ours to
inflate, and a repeat is evidence.
Evidence we cannot read, though — hops are addressed by a one-byte hash,
so a repeat is as likely two colliding nodes as a genuine loop. The packet
card and the Message paths dialog now say exactly that under the via row
and the graph, naming the repeated hops through the shared path widget.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An arrival was one line — time, SNR, then "via" and the route, all competing for
the same 72 cells, with the route (the only part that varies, and the only part
the graph above draws) last in line and first to be scrolled away.
Split in two. The route gets a lane of its own, introduced by nothing: on a line
that holds nothing else, "via" tells the reader what they can already see. Under
it hangs the frame's own reception facts — time heard, SNR, which resend — muted
and stepped in, so a pair reads as one arrival. The picked line is the route: it
is what one arrival differs from another by, so it is what ←→ scrolls and what
the ❯ points at, and the facts beneath hold their lane while it slides.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The composer's insertion point was a reverse-video arrow living between two
hops, which forced the whole preview back to plain arrows — chips fuse the seam
it needed to sit in — and made a wrapped route carry the cursor by hand, riding
a line's trailing cue when the break landed on it.
An insertion point is a position in the route, so it is now a hop of its own: a
`+` slot in the brand accent (the same colour the row cursor below it wears),
spliced in by `path_line(cursor=…)`. It measures, wraps, and colours like every
other hop, so the preview keeps whichever mode the terminal earned and can never
strand its cursor on a fold. `cursor_arrow` goes with it, out of both `PathLine`
and `path_text`.
Message paths: the arrival rows draw through the path line too — chips in each
node's hue, names alone, no hash repeated after one. The hash bytes are the
graph's job one row up, and the shared node hue ties the two together without
spelling the hex twice. The graph loses the blank line above it; its canvas
already opens with air over the topmost lane.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The dialog ranked layers by selection (priority 3 for the picked path, 2 for the
rest), so every ↑↓ re-elected the spine and re-laid the whole graph out. Rank by
first-heard order instead and move `emphasis`, the way the node detail's Routes
tab already does — the geometry is fixed by evidence, only the white lane moves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two related passes on THE route graph (ui/pathgraph).
1. **Featured path is always white.** The Message paths dialog and the
Trophy case coloured their selected/scored path by reception SNR
(theme.snr_rgb). But on a graph that draws only *one* real path over a
grey backdrop, colour carried no comparison — it just competed with the
node-name hues on the same canvas. Drop the SNR tint: the selected/walked
path reads white (the single-walk convention), the unused paths grey.
Removes the now-unused theme.snr_rgb / _SNR_RGB and updates both captions.
2. **The packet viewer draws a relayed packet's route.** A `packet` frame
that actually crossed a relay now slots the fan-lane graph between its
parsed detail (heard/from/class/via) and its extras — origin → relays →
us, the same picture Message paths and the Trophy case draw. A direct
arrival skips it (its `direct — no relays` via row already says all there
is). render_body splits into head/tail grids sharing one label-lane width
so the rows stay aligned across the graph; the dashboard threads a
node-type resolver through so relays wear their map marker (▲, …).
Full suite green (806).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three independent consistency passes:
1. **Message paths: unknown relays as grey full-hash**
The message-paths dialog showed unnamed relay hops as a single lit byte (e.g.
`e8`). Now they render at the device's path-hash-mode width, muted grey, and
annotated with their addressed byte — `e839f2 (e8)` — so the rows stay
readable and cross-reference the route graph's byte labels (matching named
hops' trace presentation). Added `hash_as_name` flag to path_text widget.
2. **Dialogs: multi-layer compositor + backdrop for empty stack**
Opening a confirm over a floating popup used to stretch that popup to
full-frame (the single-float compositor's bug). Now the compositor maintains
a pool of _MAX_DIALOG_LAYERS=8 centered-box floats, each rendering one layer
above the background — so a popup and its confirm both stay their own size.
Also: button_dialog/typed_confirm/autocomplete now float over a blank base
when the stack is empty (via _run_dialog_screen), guaranteeing popups render
as small boxes on an empty stack, not full-frame chrome. Simplified
message_dialog to delegate its backdrop logic to button_dialog.
3. **Data-loss dialogs: red `destructive` tier**
Added a second caution tier to the dialog API: danger=True stays amber
(disruptive: discard edits, reboot), destructive=True is red (irreversible
data loss). Updated: single-record delete, channel "clear slot", courier
"clear finished" (new confirm), and startup "remove device" (splash confirm).
Per UX spec: bulk/irreversible deletions gate behind typed_confirm (also red);
single-record delete is a red Cancel/[verb] button dialog.
All changes are backward-compatible. Full suite: 793 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Route graph (both the record card and the Message paths dialog):
- Relays now wear their own map marker — ▲ repeater, ■ room, ◉ sensor —
instead of a generic ●, via a node-type resolver threaded into
route_graph_style (built from contacts; an untyped relay keeps the dot).
- A shared node-type key (★ you ▲ repeater ● node ■ room ◉ sensor) sits
under every fan, minted once as widgets.node_type_legend.
- The drawn path is coloured by SNR (the SNR palette): the record's walk
by its weakest link, the Message paths selection by its reception SNR;
unselected paths stay gray. Per-path, not per-hop — overheard frames
only carry the reception reading.
Record card:
- New 'reliability' stat: the success rate of traces aimed at the walk's
far node (the one delivery figure history can answer — a failed trace
records its target, not its route), with the sample count beside it.
- Area drawing reworked per feedback: our star is drawn last so it is
never hidden, hops are pinned in their node-name hue (star stays
yellow), the fill is one flat tone (even-odd shades only the enclosed
side; no dimmer interior wash), and the box is larger.
New shared helpers: theme.snr_rgb, widgets.name_rgb,
trace_runner.make_node_type_resolver, repository.target_trace_counts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The graph now speaks in path-hash bytes: every relay gets its marker
plus the first byte of its hash in the marker's colour, while the rows
beneath carry the names annotated trace-style (YUL-Poly (3d)), so the
two halves cross-reference. Edges drop the SNR gradient — the selected
path draws white over the unused paths' gray, which is the one question
the graph is asked. Columns are the distinct depths spread evenly across
the width, height follows the fan's real reach (lanes are no longer
burned on relay-less direct paths), and labels sweep for a spot clear of
the drawn lines (MapCanvas grows avoid_dots) before overprinting one.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ^P view grows from a static scroll window into its own dialog. Up
top, every distinct path the message took drawn in the atlas's edge
style — origin left, us right, the final relay → us link coloured by
that path's reception SNR, earlier links slate — with markers for every
node but labels only along the currently selected path, so a many-path
graph stays readable; the selected path draws bright, the rest dim.
Beneath, one row per arrival with a selection cursor: ↑↓ move it (the
graph follows), and a row longer than the dialog scrolls horizontally
with ←→ — the whole line shifts under an edge …, and snaps back the
moment the selection moves on. crop_cells in tui.render is the new
cell-exact window primitive behind that.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>