Commit Graph
33 Commits
Author SHA1 Message Date
Jean-Pierre MartineauandClaude Fable 5.1 c9f5a71ce7 Every Python file names its licence with an SPDX identifier
Apache-2.0 does not require per-file headers, but a file lifted out of the tree carried no marking at all. One comment line, first (or right after a shebang), on every .py under meshterm/, tests/, packaging/ and scripts/, plus the extensionless SPI bridge; tests/test_spdx.py keeps a new file from landing without one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 13:09:18 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 356ca6a94a The widgets CLAUDE.md names as THE widgets have public names
A leading underscore says "internal to this file, don't import me", and the
column-age formatter and the node glyph table wore one while seventeen modules
imported them and CLAUDE.md named them as the one place each concept lives.
format_age sits beside format_ago now (prose age, column age), NODE_GLYPHS and
DEFAULT_GLYPH beside them, age_seconds with the ages. A mechanical rename, 140
references across 24 files, CLAUDE.md's three lines with them so the guide and
the code move as one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 01:13:18 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 ffd2649a35 Shared logic sits in the layer that reads it, and the CLI answers --version
A pre-release architecture and QA pass, applied as one change because the moves
share files and only the whole set was gate-tested (2480 passed, ruff clean).

Three new modules pull logic down out of layers that should not own it:
core/channel_probe.py (ChannelSlot, read_channel_slots, probe_channel_slots)
so services/device_state.py no longer imports a screen; services/markers.py
(gather_markers, with MapMarker a core/models dataclass) so two screens stop
importing a tool; core/tracing.py (trace_timeout, path_hash_flags) so
core/connection.py no longer imports services. The routing-prefix width is
DeviceState.routing_prefix_bytes(), replacing five private cross-module imports.

One definition per thing: is_path_hash, the NodeResolver/NameKeyResolver
aliases, TILE_RETRY_SECONDS, haversine_km, marks.SELF_MARK, MENTION; nine
private names that crossed module boundaries got public ones and menus.__all__
lists what other modules import. A preference's default is written once
(ALERT_CAP, DONE_CAP, DEFAULT_VIEW_FRACTION read the registry), the stranded
PREFERENCES doc comment sits on its subject, and three docstrings stop naming
questionary, a library dropped long ago.

The command line gains --version (eager, exit 0, documented, tested) and drops
the -p short form on trace, trace-path and tx-optimize: the root lifts -p as
--profile before a leaf ever sees it, so the leaf's could never work, and a
derived test now checks every leaf against every root short option.
tx-optimize with a one-hop --path is a usage error, exit 2, because nothing
transmitted is not a device failure. Log handlers are closed before they are
cleared (197 unclosed-file warnings gone), chat's fire-and-forget tasks keep a
reference, coroutines use get_running_loop(), the never-awaited MapScreen._load
in a test is awaited, and the CLI sweep passes --no-ble to devices so a test
run stops turning on the Bluetooth radio three times (the suite is 40 % faster).
The about page lists every dependency, and the release skill bumps the README's
download names, which still said 0.2.8.

apply_ops stays in tools/: it and its helpers build report blocks and open two
screens, so moving it to core/ would invert core over ui. The widgets CLAUDE.md
names by their private spellings (_format_age, _NODE_GLYPHS) keep them until
CLAUDE.md is edited alongside.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 19:19:45 -04:00
Jean-Pierre MartineauandClaude Opus 5 92d31a3191 Every file is spelled the way the formatter spells it
`ruff format .` across the repo: 153 files, no behaviour change. This is the
second gate CONTRIBUTING.md promises, and until now it reported 154 files needing
reformat — so a first contributor ran the command the guide told them to run and
could not tell their own diff from the background.

Landed on its own, with nothing else in it, because a whole-repo reformat is only
safe to skip past in blame if it is provably nothing but layout. It was checked
that way rather than assumed: every changed file was formatted in memory and
compared by full `ast.dump()`, and 152 of 153 come out structurally identical
(the one exception was a docstring opening on a quote character, fixed in the
commit before this one). The string and comment token streams were diffed
separately, so a quote flip or an edited comment could not hide inside a
re-layout — five quote-style normalisations and sixteen implicit-concatenation
joins, all value-identical, and one commented-out line gaining the space after
its `#`. `meshterm specimen` is byte-identical on both platforms, and the suite
is 1888 green, gallery gate and `test_theme16` included.

What actually changed, in order of how much of the diff it is: wrapped calls and
signatures rejoin onto one line where they fit inside the hundred columns; the
magic trailing comma expands what it is there to expand; complex slices gain
their spaces (`[len(noise) :]`); a few redundant parens go. The tables that are
laid out to be read were fenced in the previous commit and are untouched here.

The aligned trailing-comment column is gone from about ninety lanes that were not
fenced — including the `heat.*` ladder in `MESH_THEME`, which cannot be fenced
where it sits. Every comment stays on the entry it describes, so nothing there
reads wrong; it just no longer lines up.

Add this commit to `.git-blame-ignore-revs`, which follows immediately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 01:07:19 -04:00
Jean-Pierre MartineauandClaude Opus 5 b5006cc415 Every line fits the hundred columns it was configured for
`E501` across 69 lines, wrapped rather than exempted.

The prose ones — module docstrings in `menu`, `node_detail_screen`,
`pathline`, `pathgraph`, `walk_screen`, `trace_runner` and `test_about` — are
reflowed as paragraphs, so the block keeps its shape instead of gaining one
short orphan line. Nothing they say changed.

The code ones mostly gained a line break inside a call. Where a break would
have read worse than a name, the sub-expression got one instead: `reach` in
`topology`'s quality clamp, `plain_cells` in `pathline`, `numbering` in the
trace list, `listed` in `flash.py`'s refusal, and a small `shape`/`is_strip`
helper in two tests that were comparing or matching the same shape twice on one
line. Four docstring summaries lost a word they were not using.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 20:22:52 -04:00
Jean-Pierre MartineauandClaude Opus 5 71b16ec7b7 The annotations, imports, and dead locals answer to ruff
`ruff check --fix` over the whole tree, ahead of putting the linter in CI:
1977 mechanical fixes, overwhelmingly `Optional[X]` → `X | None` (1186 in the
package alone) and the quoted annotations that `from __future__ import
annotations` already made unnecessary, plus import sorting and unused imports.
Nothing here changes behaviour — the suite is 1885 green before and after.

Four findings were real rather than stylistic and are fixed by hand: a dead
`cells` range in `MapCanvas`, a dead slot dict in the heat-ladder test, a
`ProgressScreen` task handle nobody read, and a tab loop binding a `label` it
never used. The two `F821`s were quoted forward references to names imported
inside their function bodies — now declared under `TYPE_CHECKING`, which keeps
`test_performance`'s local import (it guards the startup import graph) while
letting the linter resolve the name.

412 findings remain and are deliberately untouched: they need a decision about
the rule, not an edit to the code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 17:26:12 -04:00
Jean-Pierre MartineauandClaude Opus 5 bcf387c620 The test fixtures stop being a picture of one person's mesh
Every node in the suite was named after hardware on JP's desk: YUL-Cartierville
and YUL-Poly are repeaters he runs, two more were the BLE
companions, another his own handle, and a long tail of YUL-Far / YUL-Me / bare
YUL besides. A stranger reading the tests learned his callsign, his city and his
gear before learning what the tests check.

They join the family the suite had already invented for exactly this — Alice,
Lounge, Yagi-Repeater, hub-1: Hilltop-Repeater, Lakeside, Waymarker, Homestead,
and Hub for the 3d-keyed node. Hub is not a new name; test_device_config already
called that contact Hub, so the suite was carrying two names for one fixture.

Replacements are the same length as what they replace, so every width budget,
truncation assertion and gallery line is unchanged. The two map tests that type
a literal query picked `yu`/`yul` to match the old name; they type `hi`/`hil`
now, same length, same single match.

Left alone on purpose: the About page and the donate link, where the handle and
the repeaters are the point, the hand-drawn splash art, and the XIAO README
reporting what JP's own radio advertises.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 02:01:55 -04:00
Jean-Pierre MartineauandClaude Opus 5 a5fd4357f1 A windowed list stops at its ends
Four screens hand their rows to a ListWindow and let the highlight drive it:
the mesh walk's links, the path composer's suggestions, the node detail's
routes, and Message paths' arrivals. All four wrapped — ↓ off the last row
landed on the first — and in a windowed list that is not one step. The window
follows the highlight, so the wrap hauls the whole list from the tail back to
the head and flips both edge markers on the way: it reads as the screen
changing under the reader rather than as a move they made (JP, 2026-09-04).

All four clamp now, the way the live feed's cursor already did and the way a
grouped select list does with `wrap=False`. Held-down arrows settle at an end.
The rule is written down where the pattern lives, on `ListWindow` itself —
each screen owns its own cursor, so the class can state it but not enforce it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 02:20:20 -04:00
Jean-Pierre MartineauandClaude Opus 5 d28240f658 An arrow atom names the arrows and nothing else
Surveying every popup dialog's hint turned up three that said the arrows were
something they aren't. The yes/no confirm's "←→/Y/N choose" put a key inside the
arrow atom that does not choose at all — Y/N answers outright — and the
autocomplete's "↑↓ Tab complete" did the same with Tab, which completes rather
than moves. Several keys may share an atom, but only when they do the same
thing: "↑↓ PgUp/PgDn scroll" and "Home/End ends" are the shape that is fine.

The composer's "←→ cursor" named the wrong thing rather than too much. The
cursor in this app is the row ❯ points at, which is what ↑↓ moves there; the
arrows move the insertion slot along the path, and "slot" is the word the rest
of the app already uses for it.

The confirm takes the ButtonDialog hint verbatim rather than a longer one of its
own — a yes/no prompt is a button row like any other, and its y/n accelerators
are already hinted by the Yes/No labels. It reads the same everywhere now, and
the Esc verb stops falling off the end of the box on a 53-column console.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 03:20:34 -04:00
Jean-Pierre MartineauandClaude Opus 5 deea9ebdd2 Esc peels a typed filter before it leaves, everywhere
Four screens carry a find-as-you-type filter and Esc meant two things across
them: the map and the mesh walk cleared the query and stayed, while the select
list and the path composer left the screen, filter and all. Typing "yul" on the
map then pressing Esc showed you the map; typing "yul" on a contact list then
pressing Esc dropped you at the main menu. Same affordance, same glyphs,
opposite outcome — and the select list is by far the most-used of the four.

They peel now, all of them: the typed query is the most recent thing the reader
entered, so the first Esc takes it back and the second leaves. The footer says
which, swapping its trailing Esc verb for "Esc clear" while a filter stands —
the word the map and the walk have always used, via a _esc_verb helper beside
the two atom-splicers that already bend that line. The composer's Esc now
layers exactly as its own backspace does: the entry first, then the screen.

This was finding D in docs/survey-navigation.md, open since the survey was
written and the last thing left telling a reader what Esc does. The rule is now
stated in CLAUDE.md under Navigation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 18:02:21 -04:00
Jean-Pierre MartineauandClaude Opus 5 602bceea3b Theme: the cursor row is white, not the wordmark's teal
The row `❯` points at borrowed `brand` — so the app's identity ink and
"the row you're on" were the same hue, and worse, teal/cyan is itself a
node hue: a cyan-keyed node's name vanished into its own highlight.

Give the highlight its own name. `cursor` is white on both platforms
(slot 15 on the VT, the top of the bright bank, so the row's bold cannot
promote it further and every dim-slot `not bold` pin inside a highlighted
row goes on meaning what it meant). White sits outside the per-node
spectrum, so it can never collide with an identity; it does share ink
with `you`, which is the accepted cost — spans that set their own colour
keep it over the base, so only unstyled text goes white, and only on the
row already under the reader's eye.

Every surface that draws its own cursor row follows: the select list and
its reorder mode, the feed, chat, routes and actions on the node page,
the composer, records, trace, tx, the walk, the paths screen, the prompt's
suggestions. The composer's insertion-slot chip takes the same white — its
docstring already tied it to "the colour the list cursor below it wears",
and the chip ink was already picked to stay readable on white.

Reverse-video chips keep `selected`: on the VT a reversed white lands on
the light grey the F-key lane already owns, so a white default button
would stop reading as the committing verb.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:51:37 -04:00
Jean-Pierre Martineau e86d6a705a Path composer: Esc already cancels, so the Cancel row goes
The row's whole implementation was `super().handle("escape")` — Esc, spelled
out as a cursor stop. It cost a keypress on every platform (End then Enter,
against one Esc), sat below the row that commits so End landed on the way
out rather than on the thing you came to do, and taught nothing: Esc leaves
every other screen in the app without being told to.

The action group now ends where the actions do — Use this path, then Auto
in target mode. Cancel stays where it earns its place: a dialog asking a
real question, where the choice is the point.
2026-08-22 14:08:22 -04:00
Jean-Pierre MartineauandClaude Opus 5 28bd7e6fd3 Compose path: the commit row names the action, not the route
"Use this path" was respelling the composed spec as raw hex, two rows under
the live preview that *is* the path — the same thing said worse, growing by a
hop every time the route did. The row now says what Enter does and lets the
picture above it say what to. Only the unarmed case still needs words.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 01:30:19 -04:00
Jean-Pierre MartineauandClaude Opus 5 6a12104cf7 Path lines: cut them all the same way, and end them where they end
Two fixes to what a path line shows at its edges and at its ends.

The edges: the crack only reached the two surfaces that drew their own marks.
Everywhere else a path still met render_to_ansi(no_wrap=True) or a plain
truncate, both of which ellipsize — so the Message paths rows you were not
on trailed off in dots while the one you were on cracked. Those cuts now go
through cut_to, which reads the line and decides: a chip breaks off on the
crack, prose keeps its ellipsis. That covers the unselected arrival rows,
every select-list row (the trophy walks and probe candidates), the TX sweep
action rows, and the composer's suggestion rows. The generic surfaces are
safe to convert precisely because the fallback is the old behaviour.

The ends: a path's ends are the nodes it went between, so a line that opens
on its first *relay* is naming a node that only passed the message on. The
Message paths rows now run origin → relays → us — the same two endpoints the
graph one row up already drew between, so the row and the picture finally
cross-read. A hopless arrival stops collapsing to the word "direct":
Alice ▶ ★ is what direct delivery looks like, on the same rails as every
relayed row.

And our own end is the ★ everywhere, not our name — it was already so on the
trace lane, the trophy card and the composer, and now also on the node
detail Routes rows, the TX sweep route, the mesh walk trail and the live
feed's subject lane. It is the one node the reader never has to be told, and
in an 18-cell feed lane or a width-starved trail the cells it gives back are
hops that stay in view.

Left alone deliberately: the packet viewer's `via` row (the dialog has a
`from` row naming the origin, so `via` really is just the relay chain), the
verbatim wire spec, and widgets._link_text — a table of FROM → TO *links*,
still on the legacy path_text builder.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 18:36:43 -04:00
Jean-Pierre MartineauandClaude Opus 5 cbdabef174 Find query: put it where the platform can actually show it
The map and the mesh walk echo the query you are typing in their footer
hint. The PicoCalc draws no footer hint -- the F-key lane took that row
-- so on the device both screens silently filtered themselves while the
reader had no way to see what they had typed.

The app already had an answer: the select list and the path composer
each draw the query as a warn-styled /query line above the rows it
narrows. That is now render.query_line, one definition, and the map and
the walk draw it too -- but only where the footer isn't, so the desktop
is untouched and no screen ever shows the query twice.

The row it costs comes from what the query narrows: the walk's link
list, the map's canvas. The map rebuilds its viewport a row shorter
through the ordinary resize path, so beginning a find loses a strip of
ground along the bottom rather than pushing the last row out of frame.

base_body_size was still mirroring only compose_base's bordered branch,
so a screen sizing itself against it left one row of a borderless frame
permanently blank -- which is the row the echo now wants back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 17:35:20 -04:00
Jean-Pierre MartineauandClaude Fable 5 f21817b816 P6: the 53-column worklist graduates - every picocalc case a hard gate
The P1 xfail list (contacts, map, walk, livefeed, remote_cli,
path_composer) turned out to be footer_hint overflows plus one real
body bug - and picocalc never draws the hint strings at all since P4's
F-key lane replaced them. So:

- The gallery asserts the footer actually drawn per platform: the hint
  string on regular, the screen's F-lane (both banks) on picocalc.
- path_composer's wrapped empty-state note emits one entry per wrapped
  line instead of smuggling a newline into a single windowed row - the
  one genuine 53-column body overflow.
- _KNOWN_WIDE is empty: all 32 picocalc gallery cases (16 screens x two
  geometries) are now hard gates. Only the pre-existing regular map
  footer xfail remains, documented as not this seam's to fix.
- First per-screen F-lane verbs, proving the override pattern: the
  contact list puts sort cycling on F3/F8, chat puts paths/retry there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 13:12:09 -04:00
Jean-Pierre MartineauandClaude Opus 5 c537fa971e Path lines: the cursor is a hop; message paths read as names
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>
2026-07-27 21:16:47 -04:00
Jean-Pierre MartineauandClaude Opus 5 fc9a459eda Path lines: notch every chip's left edge; merge duplicate hops
The gap between chips was one-sided: the previous chip tapered out to a
point onto the page, but the next one started on a flat edge. Both faces
now angle the way the path flows — the seam is two cells, a taper out and
the same point notched back inward out of the next chip's fill, with a
sliver of bare page between them. It is the notch a wrapped line already
opened on, so a fold now looks exactly like the seam it interrupted.

The composer's suggestion list could show one node twice. The evidence
graph keeps a hop hash it can't pin down as its own vertex — a 1-byte "3d"
matching two contacts stays "3d", apart from the "3d63c6429436" its wider
sightings landed on — and it is right not to guess; but once the name
resolver names them, they are one row drawn twice. They now fold onto the
longer id with their readings pooled, so the row's sample count and median
SNR describe the whole node. Merging takes both a shared name and a shared
prefix, so two unnamed hashes never fold and neither do two same-named
nodes on unrelated keys.

Folding the stub away must not fold its links away with it, so a step off
the merged node gathers from every id it reads as — otherwise merging would
quietly cost the composer whatever was only ever heard under the stub. Name
lookup and that alias walk are both memoized: they now run over the graph
on the way to each repaint, and neither the topology nor the resolver moves
while a composer is open.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 20:28:18 -04:00
Jean-Pierre MartineauandClaude Opus 5 2c2f13ce41 Path lines: gapped chips, both ends automatic, routes name nodes
The trace surfaces' route lanes were spending their width on hex nobody
reads there: every name carried the hash it is addressed by, and the wire
spec one lane below already spells that out verbatim, at the width it goes
on the air. The route lane, the planned-route preview and the composer's
live preview now name nodes and nothing else; an unnamed hop still reads as
its hash, since that is the only identity it has.

The composer's preview becomes that same lane rather than its own dialect —
pathline chips, hop-boundary wrapping, our two ends the bare star.

Both stars fade. Setting out from us and landing back on us are fixtures of
every such route, no more composed (or removable) than a mirrored return
leg, so they wear the automatic grey — dark slate as a chip, never the loud
you white. Finding the turn a wrapped route folds at now reads the faded
*tail* rather than the first faded hop, which the leading star had become.

Chips are separated, not interlocked: every seam draws its point with no
background, so the page shows through the taper and a route reads as a row
of blocks the way oh-my-posh spaces its segments. Two neighbours landing on
one fill can no longer fuse, without that being a special case.

Finally, two places that had the right information and didn't use it: the
composer's suggestion list fell back to bare hex for a short hop matching
several contacts (the topology won't guess, so it names nothing), and the
not-a-trail confirm printed whole 12-hex ids. Both now go through the
owning screen's own resolver and its chosen path-hash width, so a node
reads the same in the dialog as in the window behind it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 19:05:43 -04:00
Jean-Pierre MartineauandClaude Fable 5 e33f30d6f8 Path composer: hop-boundary preview wrap, cursor riding its seam
The pinned preview breaks between hops under a 2-column hanging indent
instead of Rich-folding mid-name at column zero; the insertion cursor
stays visible on whichever line owns its seam, including a seam that is
the break itself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 20:31:06 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 293fdeb5da Path composer: let the / key type like any character
The hop entry silently swallowed "/" — a reservation left from an earlier
design that no longer does anything (path specs use commas, and the entry
filters suggestions by name or hex). Drop the special case so "/" types
like any other printable character, e.g. to filter a name that contains one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 16:23:38 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 1b20989e6c Filters: ignore leading and trailing spaces
Every type-to-filter surface — the shared select list, the map's node
find, the atlas find, and the path composer's hop entry — now refuses a
leading space (the query never begins with whitespace) and strips the
query when matching (so a trailing space is ignored). Spaces still count
mid-query, so a multi-word name like "Homestead R&D" filters fine; only
"  Homestead" and "Homestead   " are prevented from mattering.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 16:22:47 -04:00
Jean-Pierre MartineauandClaude Fable 5 13db8c4df3 Path composer: an insertion cursor riding the route's arrows
←/→ slide a reverse-video → along the editable leg; Enter splices the
chosen hop in at the cursor and ⌫ removes the hop to its left, so a route
can be edited anywhere, not just at its end. It opens on the last arrow,
keeping the classic append flow unchanged. Suggestions, the fetch row,
and the "Next hop from" heading all follow the node left of the cursor,
and the fetch-neighbours round trip now resumes at the same position.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 18:01:15 -04:00
Jean-Pierre MartineauandClaude Fable 5 a90d93c4b3 Path composer: a grow-only dialog, and the ratchet now holds width too
The composer's box hugged its content, so every added hop, new suggestion
set, or filter keystroke resized it. It now opts into grow_only like the
packet viewer, and the frame ratchets a grow-only dialog's natural width
alongside its height, so the box holds its largest extent for the session.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 17:32:16 -04:00
Jean-Pierre MartineauandClaude Fable 5 0e93d21835 Trace records: the trail rule, a New record dialog, shallower stacks
A record walk must now be a trail — never the same link twice in the
same direction (the reverse crossing stays legal, so target boomerangs
still qualify). Repeating a stretch would let its score be farmed, so
walk_scores disqualifies such walks from every board at once; the
composer shows a yellow "not a trail" note while one is being built,
and Trace gates it behind an amber Cancel/Trace confirm. Matching
non-trail records were purged from the local database (one-off; the
rule never shipped, so no migration).

The inline "new record" note becomes a floating dialog once a run
settles (one per run, Close default, Trophy case on the left). Choosing
Trophy case resolves the whole trace session first, and the trophy
case's own Trace this path no longer reopens the browser behind the
trace screen — both directions unwind to the main menu instead of
piling up states to Esc through.

Committing the path composer also parks the action cursor on Trace, so
compose-then-Enter walks the new route; Esc leaves the cursor put.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 17:25:55 -04:00
Jean-Pierre MartineauandClaude Fable 5 94f565e61f Windowed lists everywhere the chrome should hold still
The atlas's list-in-a-fixed-screen pattern graduates to a shared
ListWindow (tui.screen): fit() settles the window, the marker rows, and
the highlight clamp together, and remembers the PgUp/PgDn stride; the
frame now records the viewport (or a dialog's budget) before render_body
runs so screens can size their chrome to it. Adopters: the dashboard
pins its charts and windows the feed; the trace screen pins route,
aggregates, and actions over a windowed results block; the path
composer pins the route preview over windowed suggestions (and gains
PgUp/PgDn); the nodes table pins its title, column header, and legend
around windowed rows. The composer's route preview and node rows also
join THE path widget, dropping its hand-rolled Name (hash) renderer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 17:23:41 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 84a2c45f12 Footer hints: one grammar, Esc always last, one key notation
The key hints now follow one sentence shape across every screen:
navigation keys first, action keys next, Esc last — the quit dialog and
TX apply dialog had Esc leading. The Esc verb is now systematic: "Esc
back" leaves a screen, "Esc close" dismisses a read-only floating view
(result windows and the share/key popups switch over), "Esc cancel"
abandons a prompt, "Esc keep" leaves a value picker unchanged. Bare
hints fill in their missing verbs ("↑↓ move", the map's and composer's
naked "Esc"), the confirm prompt says "Enter select" like its button
siblings, and key notation unifies on the compact forms the app already
half-used: ^R/^End for Ctrl chords (chat's "Ctrl-R" included) and ⌫ for
backspace (the map/atlas "Bksp").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 03:59:22 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 d1c8ed73a0 Titles speak one language: Sentence case, no emoji, — for subjects
Every screen and dialog title now follows the same three rules. Titles
are sentence-cased ("Trace — YUL-Poly", "Dashboard — mesh overview",
"Nodes"), never bare lowercase ("trace · x", "nodes", "command line ·
x", "path width", "admin login" and a dozen siblings). An em dash
introduces a title's subject or qualifier while middle dots stay for
chained status atoms ("Map · z12 · 34 nodes · 2 km across"), so the two
separators mean different things instead of being used at random. And
no title carries an emoji — the icon belongs to the menu row; a
content-sized floating dialog whose title emoji the terminal draws a
cell narrower than Rich measures leaves the border short (the bug the
Send advert popup already documented), so the Watchtower, Courier,
Time machine, identity-key, reboot, sync, restore, location, factory
reset, and advert dialogs all drop theirs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 03:55:12 -04:00
Jean-Pierre MartineauandClaude Fable 5 0ded99883f Trace splits in two: Trace target vs Trace path, with paced sampling
A trace is one walked path and "target" is pure UX (hardware-verified),
so the feature now splits along that line: Trace target keeps the
symmetric mirror-and-return flow (picker, Explore paths), while the new
Trace path skips the picker entirely and walks a hand-composed route —
no Auto, no Explore, Trace gated until a path exists, history recorded
under the "(path)" sentinel so composed walks never pollute the target
picker. The composer loses its mode toggle: a pinned target means
mirrored, no target means verbatim, and its preview opens as us → us.

Sampling returns, deliberately: a Sample count action (1/2/3/5/8) runs
that many traces per commit, paced by trace_cooldown_s between sends and
counted off on the in-flight dialog — never a burst. Actions reorder to
compose/explore/width/samples/trace with a Back row; the w/p/x hotkeys
retire.

Display: route nodes drop the underscore (prompt_toolkit draws U+00A0 as
"_", so name (hash) now joins with plain spaces), the composer renders
nodes exactly like the route line, the (previous · …) stamp gets its own
line, the path row ends with the displayed hop count, Explore rows lead
with the route instead of an "observed path" label, and the empty Traces
placeholder is gone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:33:12 -04:00
Jean-Pierre MartineauandClaude Fable 5 22b182000b Route out one way, home another: asymmetric paths, width picker, actions
Four trace-feature upgrades, all UX-preserving:

- Stored traces recover their path-hash width from the (uniform) hop
  hashes at rehydration, so a target's previous route renders short
  hashes instead of the device's full 64-char public key.
- The trace screen's verbs are selectable action rows now — Trace,
  Path width, Compose path, Explore paths. The cursor opens on Trace so
  plain Enter still just traces; p/x/w hotkeys stay on their rows, and
  the cursor only pins the view while it's actually being moved, so
  PgUp/PgDn still read a long trace log freely.
- A floating path-width dialog picks the 1/2/4/8-byte hop addressing
  (each row previews the target key with the addressed slice lit, the
  device's routing width marked). Changing it re-renders a standing
  forced path — hops widened back through their canonical hashes — and
  shapes every spec composed or explored afterwards.
- The composer's new ⇄ row toggles symmetric (return mirrors the
  outbound, the current behaviour) against asymmetric, where the whole
  walk is composed by hand — target routed *through*, outbound
  repeaters reusable, only the landing back on us implicit. Toggling
  materializes the mirror into editable hops (or regenerates it from
  what precedes the target), so the wire route never changes under the
  toggle; the planned-route preview dims only palindromic return legs.

Also: the target now carries its hash in the composer preview like
every other node, and the composer's footer hint reads briefer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:07:40 -04:00
Jean-Pierre MartineauandClaude Sonnet 5 fbcf13baf5 Forced traces walk the return leg too: no auto reflection
The trace protocol has no separate return-path field, so a forced
path (from the composer or an adopted scenario) has to spell out the
whole boomerang itself: outbound hops, the target, then those hops
mirrored back. We were only sending the outbound half — repeaters
never saw the return leg, so a forced multi-hop trace couldn't rely
on the destination reflecting it.

render_forced_spec() in topology.py is now the one place that builds
a forced-path spec; PathScenario.spec() and the composer's _spec()
both call it. The composer's live preview already showed the mirrored
return dimmed alongside the outbound hops - that display code stays,
but the trace screen's planned-route preview now splits the *actual*
wire spec at its midpoint instead of assuming it was outbound-only,
and the composer's reopen-with-current-path reseeding drops the whole
return leg instead of just the spec's last token.

Also normalized node-hash display in the composer to the spec's
path-hash width (matching the planned/completed route views) instead
of a hardcoded 6-hex-digit slice.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 22:42:08 -04:00
Jean-Pierre MartineauandClaude Fable 5 f1e1896918 Composer asks the gateway: fetch a repeater's neighbour table mid-path
The topology graph gains its fourth evidence source — the only one not
derived from our own reception. A repeater we hold admin rights on
reports its neighbour table (who it hears directly, at what SNR) when
asked over the mesh, revealing links and whole nodes our radio has
never received anything from: exactly the second vantage point the
composer runs dry without, two hops out.

Probed live against YUL-Cartierville (v1.15.0) through the Waymarker
before building: firmware answers the binary NEIGHBOURS request but
silently ignores guests, so the flow is login-first, tx-optimize style
(stored password, one-time prompt, forget on rejection). An empty table
is a normal answer — the gateway's own table was empty during the probe
(confirmed via its text CLI: '-none-') — so the UI says "ask again
later" instead of erroring.

Standing on a fetchable repeater, the composer shows a "fetch
neighbours" row with the suggestions it extends; the fetch runs under
the abortable spinner dialog, persists as a runs-row-owned snapshot
(schema v8, neighbour_reports), and the composer reopens mid-thought
with the same hops and refreshed suggestions tagged N. Refetching
supersedes the old snapshot per (repeater, neighbour) pair rather than
inflating the link's sample count: a table is current state, not an
event stream.

Verified end-to-end on hardware via MeshCoreDevice over BLE (login +
fetch + clean empty-table return) and in the mock (guest refusal,
discovery of a never-heard node, composer render + hand-off). 441
tests pass; the real database migrated v7 -> v8 cleanly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 21:25:32 -04:00
Jean-Pierre MartineauandClaude Fable 5 3502a2e09d Trace opens idle: compose the path from received topology, probe scenarios
Everything we receive carries a fragment of the mesh's shape: successful
traces crossed every link in their path (out and back — the reply retraces
the reverse path, so links read both ways), each contact's out_path is a
route the firmware distilled from received floods, and RX-logged packets
report the relay chain they rode in on. A new topology service folds those
into one undirected evidence graph (schema v7 persists packet paths).

The trace screen stops auto-starting on target pick. Enter runs a burst
under a floating tracing dialog (spinner chip, live progress, Abort); "p"
opens a hop-by-hop path composer whose suggestions are the tail's observed
neighbours, strongest link first, with raw hex entry for unseen nodes; "x"
browses ranked path scenarios (device route, direct, best observed
alternatives) and can probe them all — a measured burst per candidate,
recorded as path_candidates rows, reliability-first ranking, winner offered
for adoption.

Composed specs are outbound-only, always ending at the target's hash: the
stored data shows the firmware auto-replies along the reversed path, and
user-composed "there and back" loops (3d,f2,3d) never succeeded. Specs
collapse to a uniform per-hop width the narrowest known hash can honour.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 21:00:00 -04:00