When a row was a chip followed by text (a Courier entry: the recipient,
then what it waits for) and the text ran past the screen, the cut looked
back for a chip fill, found the chip far to the left, and drew a half
block in its colour at the right edge. The crack now forms only inside
the path that the row stamps, so a cut in plain text gets the ellipsis.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The part of a route that the user does not compose (a mirrored return
leg, the return home) drew every hop as the same dark slate chip. Each
faded chip now has the colours of its own node at FADE_CHROMA of their
chroma, at the same lightness, so it still says which node it is. The
PicoCalc has no colour of less chroma: there a faded hop keeps its label
colour and drops its fill. Arrows keep faint.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The star of our node had the hue of our key, set from the self-info at run
time. It is now the white of "you" on each surface: the map, the routes,
the lists, the legends, and the chat. The run-time colour, the device-state
hook, and the style generation of the render cache are removed. A faded
star keeps its grey.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Our chip had our hue as a light fill under dark text. It now has the fill
that each known sender gets, with a white label, and the PicoCalc can draw
that too: white on the dim twin of the hue. The hop flag is white_ink now.
A chat chip with no key keeps its grey fill, and its label is black. On the
PicoCalc it is black on the light grey of the keyless chip.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The star of our node was the yellow of the map. SELF_MARK now names a
theme style, you.mark, and the theme writes our key's hue into it when the
device-state cache reads the self-info. The context makes that connection,
so the services still do not import the UI. The map, the routes, the lists,
the legends, and the chat all draw it, and the render cache keys on a style
generation so no old bytes stay. A faded star keeps its grey.
In a chat, a sender or a mention with no key had the light blue keyless
chip. It is now quiet: the grey of plain text on that grey darkened by the
chip rule. Routes keep the keyless chip, arrow mode keeps node.unknown, and
the PicoCalc keeps the keyless pair.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A channel, a direct chat, a room, and the Messages list show our messages
under "★ Name" instead of the bare star. The chip has our key's hue turned
over: dark ink on a light fill, where each other sender is light on dark.
A mention of our name takes the same chip, as "★ @Name". The PicoCalc
cannot draw a light background, so there the chip keeps the normal
colours and the star marks it. Path lines keep the bare star.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A final pass over the docstrings and comments: "the own X of Y" is now
"Y's own X" (84 places), a navigation entry is a screen on the stack, our
node is our node, a paint is a paint, the quoted UX labels are in code
font, and a list of punctuation glyphs is in code font.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each test docstring states the behaviour that the test proves, in the
simple present. No assertion message, test id, or other string literal
changed. Four statements now agree with the code: the Trace highlight
stays on the last row, a menu count, and two others.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On the desktop and the Cardputer, a chip was the node's hue with dark
ink on it, so a node's colour moved from its name to the ground behind
it the moment a route drew it. Now the label is the hue, the colour the
name has in every list and in arrow mode, and the fill is the same hue
at 0.60 of its OKLab lightness (CHIP_LIGHTNESS): the rule the PicoCalc
draws with its dim twins, at full resolution. A hash label and an
annotation are the label colour throughout, with the lit bytes bold.
A keyless chip is the same pair in grey; our star, a faded hop and the
composer's white slot keep what they had.
The value came from two ladders over twelve hues. A fixed step down in
lightness made the blue fills almost black (blue is the wheel's darkest
hue), one lightness for every fill made the blue label hard to read on
its own ground, and 0.65 / 0.55 were too weak in blue / too dull in
yellow. Darkening at constant chroma leaves sRGB, and the clip merged
neighbouring greens into one #008000, so oklab.fitted lowers the chroma
instead and keeps the hue.
A thin seam between two chips of one colour is now drawn in the
previous chip's label colour, the light half of its own pair, on both
palettes; the shaded-fill rule (SEAM_SHADE) is gone with it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The console font gains the two powerline chevrons, drawn into the next
two donor slots (U+2559 and U+255B, which nothing draws), in both the
6x12 and the 6x8 builds. With them in its inventory, the PicoCalc's
verdict is core, so its routes are ribbons of chips like the
Cardputer's.
A chip there can only be filled from the eight background slots, since
under a 512-glyph font the VT has no bright backgrounds. So a node's
chip is the dim twin of its hue (theme.DIM_TWIN), and its label is the
hue itself: the colour the name has in every list and in arrow mode. A
hash label and an annotation are that colour throughout. A keyless chip
is white on light grey, the same pair one bank down. Our star, a faded
hop and the composer's slot have no dark grey to stand on, so they
stand on the page, the slot as a white + in a break of the ribbon. Two
chips of one colour join on a thin chevron in that colour.
Bold is brightness on the console, and Rich merges a selected row's
bold into every span on it, which turned the dim fills bright. Every
chip style there says "not bold"; the tests read the rendered row back
through the emulator's VT to hold that.
The new font takes effect once the font script runs again on the Lyra
(as root). Until then the PicoCalc's installed font lacks the chevrons.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Cardputer's panel is drawn in MeshTerm's own Terminus, which has the
powerline chevrons, yet its paths came out as arrows: the chips-or-arrows
verdict asked the terminal that started the app about its font, and on
the device that terminal is no one's, so the answer was unknown. In the
emulator it was worse, since the verdict was whatever the host terminal
could draw, a question about the wrong glass.
A handheld names its glyph inventory (Platform.font), and every frame is
folded down to that font anyway, so the inventory now answers: after the
MESHTERM_POWERLINE override, before any terminal probe. Terminus has
U+E0B0 and U+E0B1, so the Cardputer draws chips. The PicoCalc's 512-glyph
console font has neither, so its paths stay arrows, now said outright
rather than reached by not knowing. Diagnostics reads
"core (platform:cardputer-zero)". The verdict is bound on a platform
switch and its memo cleared there.
Checked by rasterizing gallery screens through the emulator's VT and
Terminus at 53x14; not yet on the device.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
SEAM_BLUR drops from 0.10 to 0.035: on a ladder of the spectrum's own pairs drawn
both ways, the solid point stays findable at one cell down to a little under two
noticeable differences, so the interlock is the ordinary seam and the thin chevron
is for fills that are the same or as good as (a mirrored leg, a keyless run, the
greens' nearest neighbours). SEAM_SHADE drops from 0.15 to 0.10: a lightness step
is what a thin line is seen by, so it needs nothing like the blur distance to read,
and 0.10 is a clear line that is still plainly the chip's own colour.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The thin chevron between two chips the reader can't tell apart is now drawn in the
previous chip's own fill stepped SEAM_SHADE darker (lighter for a dark fill) on the
fill ahead, so nothing on the ribbon is a colour that isn't a chip's. The step is
measured from whichever fill is further along, so the mark clears the fill it sits
on by the full shade however close the two are.
Whether two fills blur is now a perceptual distance, not a hue gap: ui/oklab.py is
the reference OKLab transform (from_hex, to_hex, distance, shaded), and the seam
interlocks only where the fills sit SEAM_BLUR (0.10) or more apart. The hue rule
mis-sized the wheel badly: sixteen first-byte steps measure 0.026 across the greens
and 0.165 across the cyans, so it blurred too little on one side and too much on the
other, and an sRGB distance would only have restated the hue gap, a fixed byte step
being a near-constant 55-60 of 8-bit RGB all the way round. 0.10 is five noticeable
differences, the typical 0x10 pair with a little to spare, erring toward the thin
chevron because a wrong blur still draws a legible seam and a wrong interlock draws
none. The greys need no exemption any more: the keyless grey and the faded slate sit
0.18 apart and keep their interlock on the metric alone.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Where two consecutive chips share a fill or sit under SEAM_HUE_GAP apart, the seam
was the solid point drawn bare, so the page cut a wedge out of the ribbon: a run of
near hues read as dashes, and on a light terminal the wedge was a hole. It is now
the thin chevron (U+E0B1, core powerline set) in the chip's ink on the fill ahead,
powerline's own mark for a join inside one colour. The ribbon runs on unbroken, the
join is a line drawn on it, and it wears no fill of its own so it can't be taken
for a chip; on a faded leg it takes the dim ink and recedes with what it joins.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two consecutive chips with near-identical fills — first key bytes under 0x10 apart
on the hue wheel, measured circularly — used to interlock on a chevron the eye
couldn't find, one shade of green laid on the next. They now take the same exception
an exact match does: the point drops its background and the page cuts the wedge.
The wheel is recovered from the fill itself, so an override colour and a console
slot are placed on it too; the greys (keyless, dim, our own neutral) blend only with
their exact selves, since two distinct greys still read as two chips.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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>
A packet's via field names the repeaters that forwarded it and neither the
node that sent it nor the one that heard it, yet its ribbon opened and closed
square, the mark that says the route began and ended there. PathLine takes
from_origin / to_destination: a cleared end opens on the notch or closes on
the point, and arrow mode spells it with a bare leading or trailing arrow.
The packet viewer's via chain and the TX sweep's composed relays draw both
ends open. Under the chain, the "reception describes..." note gives way to
the hop count atom the node page and trace scenarios already carry.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The chevron is the ribbon's continuation cue: it is what a wrapped line
closes with to say the path runs on past the edge, and it is the same
shape as every seam between two hops. So the last chip of a finished
route wearing one read as a route cut off mid-walk. Where the font has
the rounded caps nothing changed - they close the lozenge and always
did. Where it hasn't, the widget already squared the *opening* off and
documented that it squares both; only the close never followed. It now
appends nothing at all and the last chip's own pad is the edge.
The cell that frees up goes back to the route. `_measure` used to
report one closing figure for every line, which was true while the
close was always a point; it reports two now - what a line the path
runs on past closes with, and what the line that *ends* it closes with
- so a wrapped path measures its own last line honestly instead of
holding a cell back for a glyph that is no longer drawn. The one caller
that flows a leg of a route rather than a whole one says so, because
its final line is a continuation and still pays the point.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`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>
The last 122 `D205` sites, which takes `ruff check .` to zero.
Same shape as the package pass and just as additive: these were a paragraph
with no summary, so each one gains a plain sentence saying what is being
asserted, and every word that was there moves down into the description. The
dense ones benefit most — `test_pathline`'s 23, where "A seam is one cell, not
two" now stands alone and the chevron mechanics explain it underneath, and
`test_tui`'s wide-glyph repaint test, whose seven lines on prompt_toolkit's
relative cursor now sit under the one line that says what the test pins.
Four module docstrings (`test_advert`, `test_gallery`, `test_platforms`,
`test_repeater_admin`) opened with "X tests: a, b, and c" and now name the
subject on the first line with the list below it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`B905` over 56 sites, and the answer differs by idiom rather than by file.
21 were the sliding window `zip(xs, xs[1:])` — ragged by exactly one, on
purpose, so `strict=` is the wrong question entirely. Those become
`itertools.pairwise`, which is what they always meant and needs no flag: the
walk's SNR stops, the heat ladder, the route chains in `topology`/`records`,
every polyline in `mapcanvas`, and the graph's edge sets in `pathgraph`.
The other 35 pair sequences that must line up, so they now say `strict=True`
and a mismatch raises instead of silently truncating the longer side. Running
them under that assertion is what sorted the two groups: the first pass put
`strict=True` everywhere and 208 tests failed, all of them a sliding window.
One goes the other way. `mvt._decode_feature` reads tags off a network tile,
where an odd-length list means the tile is malformed — and this decoder answers
that with a shrug everywhere else (`if not geom_ints: return None`, bounds-
checked key and value indices). It keeps `strict=False`, now with a comment
saying why, so a bad basemap tile still can't crash the map.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`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>
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>
The head was cropped and cracked; the tail still dropped whole hops behind a ⋯,
because that mark was left over from when the fit itself was hop-based. So one
line carried two different claims about what was out of view, and the reader had
to know which end they were reading to know which one they were being told.
The trail is now one line read through a window, the shape every other path row
that outgrows its lane already has (the node page's routes, the Message paths
lanes, a trophy row under ←→): composed once at natural length, cropped at
whichever edge the walk continues past, each cut wearing cut_mark — the chip
sheared on its own half block, the faint … in arrow mode. Nothing is elided, so
nothing says it is.
←→ slide by the app's own 8-cell step rather than a hop: hops were the old fit's
unit, and a cropped line has no hop boundaries to land on. cut_from goes with
them — the window covers the unscrolled case in the same six lines.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The breadcrumb fitted itself with ellipsized(ELIDE_HEAD): the head hop went
behind a leading ⋯ and whatever fitted after it snapped right. That mark cost
three cells and, worse, the whole hop it was standing in for — a 46-cell lane
opened on "⋯ › Repeater-06" when it had room for the tail of Repeater-05 too.
The trophy case has always cut its rows the other way: composed at natural
length, handed over whole, cracked at the lane's edge in the sheared chip's own
fill. Same crop here, mirrored to the end the trail is anchored on — cut_from,
cut_to's head-side counterpart, added to the widget so no surface hand-rolls it.
The trail draws whole while whole fits and crops its beginning when it doesn't;
a crop lands exactly on the width, so the right snap comes free.
Each end now carries the mark that is true of it: the head opens on the crack,
which says this segment continues, and the scrolled tail keeps the ⋯, which
says whole hops are missing — because off that end they really are.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A picked row folds its node hues to the cursor white so the row reads as one
thing rather than as a name arguing with its own selection. On a path line that
is wrong: there the hue is not decoration on a name, it is what tells one hop
from the next — and what a route graph above the row is cross-referenced by,
its labels being a marker and one byte of hash. Chips were never folded (their
fills are outside the fold's vocabulary), so the bug was the arrow form: every
path line on the PicoCalc, and on any terminal without the powerline glyphs.
PathLine now stamps every shape it mints with PATH_INK, a style that draws
nothing; Text.append_text carries the stamp into the row as a span over the
hops, and the fold skips identity spans lying inside one. So the Message paths
row keeps each hop in the colour its graph label wears, and so do the TX
screen's "Route — via", the trophy board's walk and the feed's sender → recipient
lane. The node detail's Routes tab, which bought this by hand (no cursor base at
all), is untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
Three things the path rows owed their readers.
The opens-further `…` was being reserved out of the lane, so a route that
fitted exactly cracked on its own last chip — the row claiming the walk ran
on when it had arrived, the crack being the closing cap with half of it taken
away. It now rides outside the budget (`with_action_mark`, and `cut_to(...,
action=True)`): the path is fitted to the whole lane and the mark takes the
leftovers, or goes unshown. That is the widget's answer everywhere a path row
carries it, not the node page's local trade.
Every stats line hanging under a path now leads with the hop count
(`hops_atom`) — the figure the picture above encodes but never states, and
the first thing two routes are compared on. `direct` where there are none,
which is the app's own word for it; the Explore list's direct-shot provenance
tag was that same word for the same reason, so the atom absorbs it.
And Explore paths cuts its unselected candidates instead of middle-eliding
them, so walking the cursor no longer rewrites the row under it. The `⋯`
rescue saves a route's two endpoints; here they are the same two on every row
by construction, so it spent cells on what the reader knows and took them off
the candidates' front. The probe-results rows pin their ranking lanes out of
the ←→ scroll while they are at it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three cuts to what a chip line spends its cells on.
The seam was two: the previous chip tapering onto the page, the next
notching back out of its own fill, a sliver of bare page between. That gap
was insurance against two neighbours landing on one colour and fusing into a
single block — but the hue wheel is 256 distinct values wide (measured: zero
collisions across all 256 first bytes), so it was insurance against
something that does not happen. One interlocked chevron now, the chip behind
as foreground and the chip ahead as background.
Where two fills really are equal the interlock cannot draw a chevron at all,
and that case is not bad luck: a mirrored return leg is a run of identically
faded hops by construction, and so is a stretch of keyless greys. There the
point drops its background and the page cuts the wedge — still one cell, the
old separation without the old width.
The elision was a chip: pads, a fill, and a seam on each side, seven cells to
say "some hops are missing" on a line that is short of room by definition. It
is not a node called ⋯. It now breaks the ribbon instead — the chip before
closes onto the page, the mark sits there bare, the chip after opens on its
notch. Three cells, and elision_hop() is the one definition the widget and
the mesh walk's trail share. Arrow mode needed no change: a bare label
between two arrows already was a gap.
The you chip was ` ★ ` in dark ink on pure white — five cells shouting the
one node the reader never has to be told. Now the map's own yellow star
(taken from marks.SELF_MARK, so the star in a route and the star on the map
cannot drift) on a neutral dark grey, and padless, because the pads exist to
keep a *word* off the chevrons and a single mark centred in its cell needs no
such room. Three cells.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An ellipsis at a path line's edge says a word was shortened. What actually
ran out is the lane — the route continues, and in chip mode there is a
segment right there to say so with. A cut chip now breaks off on a half
block drawn in that chip's own fill and no background, so the cell is half
segment and half bare page: the crack reads as a chip that carries on.
CRACK_TAIL (▌) closes a line the path runs past, CRACK_HEAD (▐) opens one
whose start is off to the left.
Only chips can crack, which is also the whole mode test: cut_mark reads the
fill from the nearest cell still drawn — scanning back for a tail cut,
forward for a head cut, so a cut landing on a seam still takes the colour
the reader can see — and falls back to the muted ellipsis when it finds
none. An arrow line has no fill to shear, so it keeps cutting exactly as
before and no caller has to know which mode drew the line.
Applied at the four places a path line is cut rather than folded: the
ellipsized() last resort, the over-wide lone hop in wrapped(), and the two
h-scroll surfaces whose edge marks were literal "…" — the Message paths
selected row and the node detail Routes row (whose unselected rows now go
through cut_to as well). Untouched: the ⋯ that marks whole hops elided out
of a path's middle, which is a different claim entirely.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A board of records is a list of routes, and the routes were the part with no room
left. Every row spent thirty-odd cells before the first hop — `#1 Jul 12 14:30`,
a score padded to a fixed twelve — and then opened the walk by naming our own node
in full, `Homestead (cc)`, twice, on every row, on a screen where every record
starts and ends on us by construction.
So the rows draw on THE path widget with both ends bare: the app-wide ★, which the
graph on the record card already plants on us and its legend already reads back as
"you". The stars keep the `you` white — path_line's fade means "not yours to
compose", and nothing on a board of walks already made is being composed, which is
what `dim_self` now says. The lanes in front are held to what they actually say:
the day the record was set (the card carries the time), and a score lane fitted to
the widest score on *that* board rather than to the worst case everywhere.
The record card was carrying the same kind of slack. Its route ran under a `route`
label lane, hanging in the twelve cells left over — under a graph captioned
"you → … → you", where the line can only be the route. It now runs unlabelled
across the whole card, wrapping at hop boundaries, on the same widget as the rows,
with the same ★ at both ends.
And the card was three rows taller than its content. The route graph took the
shared widget's five-row floor, which reserves room for a fan of alternatives; one
walk is one lane and never fans, so it draws in three — and both stacked blocks now
hand back the rows nothing landed on. The area drawing keeps two rows in hand for a
tall shape, and a walk that comes out flat never reaches them: that, plus the
graph's unused label row, is where the three blank lines under `round trip` came
from. Trimming happens after the drawing, so nothing placed is ever lost.
Co-Authored-By: Claude Opus 5 (1M context) <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>
The breadcrumb across the top was hand-rolling its own hop sequence — its own
separator loop, its own width arithmetic, its own head-dropping. A walk *is* a
path, so it now renders through PathLine like every other hop sequence in the
app: powerline chips wherever the terminal draws them, the trail's own › arrows
where it can't, each hop in its node's key-derived hue.
It stays one line — no wrap, no scroll. When the walk outgrows the width the fit
eats into its head rather than a route's usual tail, and what survives snaps
flush to the right edge, so the focus and the steps that just led to it are the
ones always in view.
PathLine.ellipsized grew the parameter that makes that possible: `elide` names
the side the ⋯ eats into. ELIDE_TAIL (the default, unchanged behaviour) anchors
on the head and rescues the destination off the far end, so both endpoints of a
route survive; ELIDE_HEAD anchors on the tail and spares the origin nothing.
Along the way the trail stops losing its colour: the old focus emphasis built
"bold you" / "bold muted" style strings, which Rich can't parse and silently
renders as no style at all — so our own node at the root of the walk was drawing
plain instead of white. Identity styling now comes from the hops themselves.
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 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>
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>
A folded route hung flush under the value column, so a two-line route and
two stacked values looked alike at a glance. Continuations now step in
WRAP_OFFSET columns past the indent; the packer charges for the step, so
every line still fits its width. The continuation marks — the trailing
arrow, the notched chip edge — still say it in detail; the step says it
from the block's shape alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
bare_self now stands our node on the app-wide ★ rather than stripping it
to nothing, so both ends of a route are marked, not merely implied. The
star we set out from wears the you white; the one we land back on fades,
because coming home closes every route automatically — nobody composed
that leg, same reasoning that dims a mirrored return. A lone faded tail
no longer counts as the route's turn, so a walked boomerang still folds
at its own middle.
The wrapped line's opening cap carried the previous chip's colour into
the new line. It now draws the point in the line's own fill under reverse
video, which paints the notch in the terminal's own background — the only
way to name the page's colour without knowing it — so the carry-on mark
holds no trace of the chip above.
And the extended powerline block finally earns its detection: where the
font is a full Nerd Font patch (powerline_full), a path's two outer ends
round off into a lozenge. Core-only terminals square the opening and
point the close exactly as before, so nobody sees tofu.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A wrapped line was opening on U+E0B2, which points backwards — it read as
an arrow going the wrong way, not as a carry-on. It now reopens the seam
the break interrupted: the same right-facing point, arriving out of the
previous line's last fill into this line's first chip. U+E0B0 is once
again the only glyph the widget draws; what varies is whether the point
lands on a field (a seam) or on the page (a closing edge), which is also
the same-fill gap rule, now factored into _seam().
A bare hop keeps the one-cell padding every chip wears — it has no words
between them, not no chip — so our end of a route reads as a stub of the
you white rather than as nothing at all.
Explore's candidate paths go bare too: every candidate starts from us, so
the word was the same on every row and the cells are the candidates'.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An interlocked seam is the point in the previous chip's fill drawn on the
next one's — invisible when the two fills match, so a hue collision, a run
of dimmed hops, or two keyless greys read as one fused block instead of
two nodes. Those seams now draw over the page instead, leaving the sliver
of background that says where one chip ends and the next begins.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A hop with no label now draws as its seam alone — the arrow, no chip, no
word. The trace screens' route lane uses it (bare_self) for both ends of
every walk: a trace leaves us and comes back to us, so naming ourselves
twice only pushed the hops that *are* news out of the lane. Arrow mode
drops the padding the vacated label sat in ("→ 3d → f2 →"); chip mode
lets the seam point into the page.
Wrapped chip lines now open on the female point (U+E0B2, core set) from
the second line down, so a continuation reads as one — only the very
first chip of a path keeps its square left edge. The packer pays for that
cell, so lines still fit their column.
Also carries the round already sitting in the tree: the wrap evens hops
across the fewest lines it can take and folds at the route's turn, the
trace/node-detail/select surfaces route their rows through the widget.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
path_line() builds a PathLine from path_text's exact vocabulary (verified
character-for-character in tests), wrapped() carries the composer's
insertion cursor — riding the trailing arrow when its seam is the line
break — and conftest pins MESHTERM_POWERLINE=0 so screen assertions never
depend on which terminal runs the suite.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PathLine renders PathHop sequences three ways (full line, middle-elided to
a width, hop-boundary wrap under a hanging indent) in two looks: the
app-wide muted-arrow presentation, or interlocking powerline chips filled
with each node's key hue when the terminal can draw U+E0B0 (auto mode asks
termfont). Same words in both modes; call sites migrate separately.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>