The radio's RX log has recorded every frame's relay path all along;
now a chat message can be asked how it got here. ^P floats a paths
window for the picked (or latest) message: one row per logged arrival
— time, reception SNR, and the relay chain through the shared compact
path widget — so a channel broadcast that reached us three ways reads
as three rows over two distinct paths, resends labelled.
Channel messages are matched by evidence, not guesswork: overheard
GRP_TXT frames decrypt under the channel's own key (fetched from the
device when the conversation didn't carry it) and match the message by
content, tolerating the "Name: " sender-prefix convention on the wire
— our own broadcasts included, since repeaters' rebroadcasts of us are
overheard like anything else. Direct frames are ECDH-encrypted, so the
view offers the window's TXT_MSG frames matched by time alone and says
so plainly. The sender-prefix parser moves to core.channels where the
matcher, the transcript, and the dashboard feed now all share it, and
the repository gains the windowed packet_frames_between query.
Both chat kinds now share one message-pick model: ↑ picks, the pick
walks with ↑↓/PgUp/PgDn/^Home and day-jumps with ^PgUp/^PgDn, ^End
returns to compose. Enter on a pick replies in a channel (unchanged)
and opens the paths in a direct chat, each with its banner cue.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three button/status stragglers join the house style. The tracing
dialog's "❯ [ Abort ]" — the one control drawn with a pointer and
brackets — becomes the centered reverse-video chip every other popup
button already is (ButtonDialog, the reconnect dialog's Abort). Chat's
✅/❌ delivery emoji become the app-wide ✓/✗ status marks in their
ok/err styles: the emoji belong to the packet-class icon lane, and the
single-width marks keep transcript rows evenly spaced. And the channel
manager's Clear — the last bare Yes/No confirm in the app — becomes a
standard destructive dialog (Cancel left, Clear right and default,
danger theming) like every other consequence gate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Two rounds of interactive-polish fixes across the dashboard, Time Machine,
chat, and the menu popups.
Dashboard
- Feed name lane names what a row actually is: a channel message's parsed
sender, or a relayed packet's payload class (channel text / trace / …),
instead of a bare "?" or a redundant "ch N". The class is persisted so
history-seeded rows carry it too (observations.payload_typename, schema v10).
- PgUp/PgDn walk the feed selection so the highlight travels with the view.
Packet viewer
- Takes a live source and re-locates the viewed packet by identity, so packets
arriving after it opens become reachable; the n/total title stays current.
- Decrypts overheard channel text straight from stored history (crypto fields
persisted: chan_hash/cipher_mac/crypted, schema v9).
Time Machine
- Day charts label their bars with column ticks: each date sits under its own
bar with a ┬ notch, shortens (month only on the first tick and on rollover),
and thins to fit. axis_chart grows an optional ticks path for columnar charts.
- Node history charts render through the shared axis_chart widget; the mesh
Rhythm is 96 fifteen-minute slices at 4x width, "now/newest" hints dropped.
Chat
- The byte counter pins to the right edge of the input's last line, so it stays
bottom-right when the compose wraps instead of trailing the cursor.
Menu popups
- The per-node Rules popover gets an explicit Close row.
- The Send advert popup drops the emoji from its floating title (a terminal that
paints it narrow left the content-sized dialog's border short, bleeding the
backdrop through); the menu row keeps its icon.
625 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The counter was its own last body line, and the pinned day-divider
header reserves the top viewport row without relaxing the bottom scroll
clamp - so exactly when the transcript filled the screen, the counter
fell off the bottom. The budget now trails the compose text inline
(wrapping along with it, costing no row), and the frame slides the
window down a row when a sticky header is pinned at the very bottom,
so the last body line always wins over the duplicate heading.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
highlighted_hash grows a width budget - shorter than the key shows an
ellipsis, shorter keys pad so lanes align - making it THE hash widget.
The chat sender palette moves to theme.name_style as the app-wide node
name colour rule; our own node is always the pure-white "you" style
(the nodes table's self row now follows it).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A direct message was transmitted once and marked failed the moment its
single delivery-ack window lapsed, leaving Ctrl-R as the only recourse.
Now the send path re-sends an unacknowledged message automatically before
giving up, matching the mesh convention of a few tries.
_deliver_direct loops send_direct_message until an ack lands or the budget
is spent; both the initial send and the manual Ctrl-R resend route through
it. Each attempt already blocks a full ack window before the next, so the
retries are spaced by that window rather than hammering the radio, and a
hard send rejection propagates without retry.
The budget is the new direct_message_soft_retries setting: retries + 1
tries, clamped to 0..2 so a message is never re-broadcast more than twice
(0 = one shot, default 2 = up to three tries).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make sticky headers a reusable Screen feature and lift paging and
section jumps onto the base Screen, so every scrollable surface shares
one implementation: the frame records each render's body height and
viewport, and the shared helpers page by a screenful, clamp to the
content, and walk between the boundaries a screen records in
_sticky_headers (a select list's group dividers, the chat transcript's
day dividers).
Remap keys to match a good editor:
- PageUp/PageDown scroll by a screenful rather than a fixed step.
- Ctrl+Home/End reach the top/bottom (the live tail in chat).
- Ctrl+PageUp/PageDown jump to the current/previous and next section.
- In chat, Home/End and Ctrl+Left/Right act on the compose line; word
motion (start of current word, else previous/next) is added to the
shared line editor, so every text prompt gains it too.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Color-code the chat picker rows: a contact's dot takes that person's
chat hue (filled once talked-to, hollow before), previews light their
sender names and @mentions in-hue, and a relative age column sits
between the unread badge and the message text.
Pin a grouped list's section heading to the top row once it scrolls off
(sticky header in the frame), add PageUp/PageDown to SelectScreen, and
drop arrow wraparound in the conversation picker so it doesn't jump
across the section dividers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wrapped message bodies now align their continuation lines under the body
of the first line instead of falling back to the timestamp gutter, via a
new render_hanging helper. And an @[Name] token primed by the reply flow
renders as a bare @Name in that sender's stable hue, so a mention reads
cleanly and is tied to the person it names.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the static ⏳ hourglass on an in-flight direct message with the
reusable tui.Spinner, animated for the whole wait-for-ack window:
- _delivery_glyph is now a method returning the spinner's current frame when
acked is None (✅/❌ once resolved); the _SENDING constant is gone.
- _spin_while wraps the awaited send and retry, running a background ticker
that advances the shared spinner and repaints, then cancels-and-awaits it
(same no-stray-task discipline as the startup splash). Only one send or
retry is ever in flight (both gated by _sending), so one spinner suffices.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rename the package directory, imports, brand strings, env-var prefixes,
console-script entry point, and the config/data dir (~/.config/meshterm).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Channel messages carry only a slot index on the wire, which the firmware
assigns from the channel's current position. The old idx->identity cache
was trusted on a hit, so a slot reorder or re-key (in-app, via the CLI/
config tool, or on another client) left the cache stale and misfiled
later messages under the wrong channel.
channel_id_for now reads the occupying slot fresh from the device on
every call, so identity always reflects who is actually in the slot right
now; _channel_ids is demoted to a priming/backfill warm map and a
read-failure fallback. Inbound recording moves onto a single serialized
worker draining an asyncio.Queue, so channel messages still persist in
arrival order despite each needing an async device read, and a slow read
can no longer stall the event pump.
Second root cause, same symptom: a Public message could still land in
another channel (e.g. #bots) even after the fresh-read fix. The meshcore
library's get_channel waits for "the next CHANNEL_INFO event" with no
correlation to the slot it asked for, and the dispatcher fans that event
to every in-flight waiter. Two concurrent get_channel calls therefore both
resolve on whichever response arrives first, and one caller silently
receives the other's channel — which fired whenever the chat worker's read
raced any other channel read (conversation-list builder, channels screen,
refresh_channels). Fixed in the device wrapper: a per-device asyncio.Lock
serializes channel reads so at most one is outstanding and the response is
unambiguously ours, plus get_channel now validates the response's
channel_idx matches the requested slot and raises rather than handing back
foreign channel data (the chat layer then falls back to the last-known
identity instead of misrouting).
Adds tests for filing by the current slot occupant after an out-of-band
reorder, for arrival-order preservation across a burst, and for the
serialized/index-validated channel read (no cross-talk, mismatch rejected).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three interrelated improvements to the chat/channel experience:
Outgoing byte budget & unified transcript (ui/chat.py, ui/tui/prompt.py):
- Cap outgoing messages at MeshCore's UTF-8 limits (150B direct, 130B
channel) and block over-limit sends, keeping the buffer intact to trim.
- Show a live `used/limit` gauge in the compose bar whose used count steps
green→yellow→orange→red by bytes remaining, with truer yellow/orange hues.
- Color the overflow characters red in the compose line (multibyte-safe).
- Render direct chats with the same grouped, per-sender layout as channels
(headers, day dividers) instead of one line per message.
Direct-message delivery retry (services/chat_service.py, persistence,
core/models.py, tools/chat.py, ui/tui/session.py):
- Track outbound acked state (None awaiting / True / False) with a persisted
row_id, add resend_direct + update_chat_ack, and wire Ctrl-R to retry the
latest unacknowledged message in place with ⏳/✅/❌ delivery glyphs.
Channel slot capacity discovery (core/channels.py, core/connection.py,
ui/channels.py, tools):
- Discover the firmware's real channel-slot count via a bounded read-only
probe (Device.channel_capacity) instead of assuming 8; show used/total in
the manager and refresh the chat slot→identity cache after any mutation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Store channel messages under a slot-independent channel identity (sha256 of
the effective secret) instead of the raw slot index, so reordering or re-keying
slots no longer scrambles history. DB schema v3->v4 adds a channel_id column
with an idempotent migration and a lazy backfill that maps legacy rows to the
channel currently in each slot.
Recognize the firmware's fixed-key default "Public" channel as public: add
DEFAULT_PUBLIC_SECRET / KNOWN_PUBLIC_SECRETS and split is_name_derived (key
reproducible from the name, so it can be re-derived on write) from
is_public_channel (shared meshwide). The default channel no longer mislabels
itself as private, and its well-known key is preserved on edit/reorder.
Lead each chat picker row with a glyph: the channel's openness marker
(# name-derived, globe fixed-key public, lock private) or, for a direct chat,
a speech balloon once we've exchanged messages and a person before. A shared
channel_glyph() keeps the channel manager and the picker in sync.
Round out the messaging UX: live callable row titles and plain-text filtering
in the TUI select screen, plus assorted device-picker, monitor, route-map, and
config-editor menu refinements.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce channel and direct messaging: an always-on ChatService that
records inbound messages to history, a live full-screen chat screen, and
a chat tool (interactive picker plus send/history/list/listen CLI). This
bumps the DB schema to v3 and adds the supporting models, repository
queries, and connection wiring.
Also polish the monitor/menu UX:
- Stop console log handlers from corrupting the full-screen TUI by
detaching the RichHandler for the life of the session (file logging
continues); most visible when toggling the monitor.
- Colour only the header's monitor glyph (green ● on, red ○ off).
- Crop the header to a single line instead of wrapping on narrow terminals.
- Drop the redundant toggle-confirmation window so toggling returns
straight to the main menu.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>