Commit Graph
83 Commits
Author SHA1 Message Date
Jean-Pierre MartineauandClaude Fable 5 ba1ff2eedf Sparkline becomes a 2-hour live pulse, newest at the left
The activity window shrinks from a week to two hours in 24 five-minute
buckets (12 braille cells), bucketed newest-first so "now" sits at the
left edge and traffic slides right as it ages. Bar heights switch from
peak-relative scaling to absolute tiers (1/3/8/21 messages per dot), so
the same traffic draws the same bar on every row, and silent cells drop
to a faint two-bottom-dot baseline — a quiet channel reads as a flatline
rather than a gap. The Reorder row's narrow ↕ arrow gains a second space
so its label aligns with the two-cell glyphs of its sibling rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 04:01:59 -04:00
Jean-Pierre MartineauandClaude Fable 5 96813130d5 Reorder screen slims to glyph+name rows and hugs its content
Enter is now the sole grab/drop key (Space is ignored), the rows drop
the openness/hash lanes since reordering is about position, and the
screen gains a dialog_width sized for its fullest state so the popup
fits its content without resizing mid-interaction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 03:32:23 -04:00
Jean-Pierre MartineauandClaude Fable 5 8c75aea3b0 Reorder screen grows Apply/Back action rows below the list
Follow the device-config editor's exit-group pattern: once the order
has actually changed, an ok-tinted Apply joins an err-tinted
"Back — discard changes"; untouched, a lone Back. Enter on a list row
now grabs/drops like Space (matching Enter-activates-the-row), and
committing happens on the Apply row instead of Enter-anywhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 03:14:34 -04:00
Jean-Pierre MartineauandClaude Fable 5 1d62691599 Reorder screen gains OK/Cancel: Space grabs, Enter commits, Esc aborts
Esc used to be the only way out of the channel-reorder screen and it
always committed, so there was no way to back out of an accidental
shuffle. The screen now follows the other dialogs' Enter-accepts/
Esc-cancels contract: Space grabs and drops the highlighted row
(arriving as printable text from the session, with the normalized
"space" action accepted for symmetry), Enter resolves the final order
— dropping a still-carried row where it sits — and Esc resolves CANCEL,
which the session already maps back to the identity order so the
caller treats it as "no change". Footer hints re-phrase per state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 03:02:30 -04:00
Jean-Pierre MartineauandClaude Fable 5 7ed31c8f31 Activity lane becomes a 16-bucket braille sparkline
The three-threshold bar meter compressed a week of traffic into one of
four states; braille cells pack two dot columns of four rows each, so
the same 8-cell lane now draws a real 16-point histogram — each bucket
~10.5 hours — and a channel's rhythm (bursts, lulls, decay) is legible
at a glance. channel_stats() gains the per-bucket histogram, bucketing
the window's timestamps in Python off one filtered query; recent is
now simply the histogram's sum.

Bars scale to the row's own busiest bucket — the sparkline shows shape,
not cross-channel volume (the MSGS lane does that) — with non-empty
buckets floored at one dot so a lone message never vanishes. A silent
window keeps the muted ······· placeholder.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 02:53:28 -04:00
Jean-Pierre MartineauandClaude Fable 5 df9cdc4f2f Channel list grows message vitals in config-editor lanes
The channel manager's flat name/hash rows become fixed, column-aligned
lanes under one header line — name, openness, hash, unread badge, total
messages, last-message age, and a three-bar activity meter over the
trailing week — so a glance shows which channels are alive, not just
which exist. A new Repository.channel_stats() aggregates totals, the
week's slice, and the last timestamp per channel identity in one SQL
pass; rows read it through a small TTL cache with callable titles, so
a message arriving while the list sits open updates its row in place
(the conversation picker's trick, unread badge styling included).

Both menus adopt the config editor's persistent-backdrop pattern: the
list stays pushed while every sub-prompt floats over it as a modal
popup instead of replacing the screen. The detail screen leads with a
vital-signs line (slot, totals, unread, last activity) and lays its
actions out in the Device-actions two-lane style, with the live unread
badge on Open in chat and the destructive Clear row tinted err.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 02:40:04 -04:00
Jean-Pierre MartineauandClaude Fable 5 0f49cf0a2d Passive monitoring is always on; CLI monitor becomes a timed capture
The on/off switch never made sense for a pure listener: recording is
now unconditional, so the toggle submenu, the persisted preference
(MonitorStore/monitor.json), and the header's ON/OFF framing are gone.
The monitor registers its hub subscription at startup — a device-free
step that cannot fail — and its run row opens lazily on the first
observation, so recording follows the hub wherever it opens (eagerly,
or deferred by connect_on_start) and silent sessions leave no empty
runs. The menu entry is now just the read side, "Heard nodes", and the
header shows live heard counters instead of a switch state.

meshterm monitor on the CLI drops --on/--off for a bounded foreground
capture (--seconds N, 0 = until Ctrl-C, mirroring chat listen): it
tails each overheard packet live and summarizes the window's heard
nodes when it ends, while persisting history exactly like a session.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 02:07:23 -04:00
Jean-Pierre MartineauandClaude Fable 5 12188afaa0 Light every frame from its top-left corner
A new glow pass post-processes composed ANSI lines: it finds each
box's top-left corner glyph, lifts it toward white, and fades the
highlight quadratically along the top and left edges into the
border's own colour. It reads colours from the SGR stream itself, so
accent, muted, and warn frames each glow in their own hue and nested
tool panels are lit individually with no per-callsite wiring.

String surgery instead of a Rich parse/re-render round-trip keeps the
pass ~2ms per repaint. Replaces render_to_ansi_dialog, whose only
caller now composes through the glow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 01:51:59 -04:00
Jean-Pierre MartineauandClaude Fable 5 b0cd2b0ccf Anchor splash at 2/5 height so the populated picker reads centered
The banner was pinned so the box top sat at the terminal midline; the
box only grows downward, so a populated device list made the splash
bottom-heavy. Anchoring at 2/5 centers the grown block and leaves the
brief small states slightly high, the usual optical dialog placement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 01:43:18 -04:00
Jean-Pierre MartineauandClaude Fable 5 bca0d7f603 Aerate splash boxes; startup picker hint says Esc quits
The startup picker's footer read "Esc skip", but Esc there exits the
program (same as the Quit row) — say "Esc quit".

Extract the floating dialog's breathing-room rule (a blank row above
and below the body whenever it costs no visible content) into a shared
_breathing_room helper and apply it to the chromeless splash boxes too
(device picker, PIN dialog, message/spinner splashes), so every popup
box is aerated alike. Full-screen panels stay flush on purpose.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 01:40:29 -04:00
Jean-Pierre MartineauandClaude Fable 5 3a972ea743 Short outcomes pop as OK dialogs instead of filling the result frame
A one-line result like "✓ flood advertisement sent" used to open the
full scrollable result window — a huge frame for a single sentence.
TuiUi.present() now collapses small, text-only output (at most 3 lines,
none wider than 76 cells) into a centered popup with an OK button,
floating over whatever screen is beneath; tables, long errors, and
multi-line output keep the window. Fixed centrally in present(), the
upgrade covers every such site at once: adverts, clock sync, backups,
key import, "applied N changes", tool failures, and future ones.

ButtonDialog learns to render a pre-styled (multi-line) Text prompt so
note markup survives into the popup, and the border echoes the message's
strongest tone (err > warn > accent). When the popup would sit on an
empty stack (a tool run from the popped main menu), a blank base frame
is pushed beneath it first — the same trick as the reconnect dialog —
so it draws as a floating box, not as the base.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 01:32:58 -04:00
Jean-Pierre MartineauandClaude Fable 5 920b3f8b83 Promote Send advert to the main menu as its own tool
Announcing the node is the everyday operation in the Device actions
bucket, but it sat two screens deep and out of reach of the main menu's
type-to-filter. Give it a top-level entry (order 10, right after Device
actions; Device info shifts to 11) via a new menu-only tool that reuses
the existing advert flow through a standalone send_advert() entry point.
Only SELF_INFO is read on open — the advert op and contact card never
need the full snapshot — so it stays snappy over Bluetooth. The row
inside Device actions stays, and scripting remains under `config
advert` / `config share`, so no new CLI subcommand.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 01:27:41 -04:00
Jean-Pierre MartineauandClaude Fable 5 e4a4e7c03b Tuning params: real float units per firmware; drop no-op TX delays
Checked against companion firmware (MyMesh.cpp CMD_SET_TUNING_PARAMS):
rx_delay_base and airtime_factor are floats carried x1000 on the wire
(constrained to 0-20 s and 0-9 respectively), so the settings now speak
real units — previously the raw wire integers were shown and edited,
making "2" mean 0.002.

The TX delay factors added in the previous commit are removed: the
companion handler reads exactly the two fields and ignores trailing
bytes, so they were silent no-ops. They exist only as remote-CLI
settings on repeater/room-server/sensor firmware.

Airtime factor now leads the Tuning section, ahead of RX delay.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 01:14:05 -04:00
Jean-Pierre MartineauandClaude Fable 5 42a22fd228 Expand device config: new settings, sync clock, and a live status panel
New staged settings: contact auto-add bitmask, default flood scope
(hashtag-normalized, 31-byte slot), and the two TX delay factors the
library always zeroed (write-only on hardware, so unknown values fall
back to 0 exactly as before). TX power is now capped by the board's
reported max_tx_power instead of a hardcoded 22, with the prompt hint
and validation sharing one effective_maximum.

Device actions gain Sync clock (drift shown before writing host time;
also a config sync-clock CLI command), and Device info now leads with
a live Status panel: role, model/firmware, battery, storage, clock
drift, uptime, and radio/packet statistics — every read best-effort so
older firmware still renders.

The simulator models all of it, including a seeded clock drift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 00:20:15 -04:00
Jean-Pierre MartineauandClaude Fable 5 8839e4cf70 Section headers: Capitalized and accent-highlighted, not UPPERCASE
Separator gains a style field (default muted) so section headings can
carry the accent highlight while blank spacers, column-header lines,
and inline notes stay muted. Panel titles and inline section labels
(Trace summary, Route, Busiest links, ...) follow suit; column headers
alone keep the UPPERCASE convention.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 23:58:13 -04:00
Jean-Pierre MartineauandClaude Fable 5 768487e456 Polish menus and dialogs: lanes, headers, and a Device actions screen
- Dialog footer hints take the border's hue one shade darker (hint.*
  theme styles), mirroring the brighter title tinting.
- Dialogs get a blank breathing row above and below the body whenever
  it doesn't cost visible content.
- The chat conversation picker gains column headers over its lanes.
- The map screen titles itself "Map", matching its menu item.
- The config editor lays settings out in aligned SETTING / VALUE /
  DESCRIPTION columns under a header line, and with changes staged the
  Apply and Back rows sit together below the blank separator.
- Device actions move out of the config editor into their own screen
  behind a new menu-only device-actions tool (CLI equivalents stay
  under the config group).
- Type-to-filter keeps section headers (and column headers) in place,
  and section jumps now work over the filtered rows too.
- The main menu aligns tool names and muted descriptions into two
  columns, headerless.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 23:10:11 -04:00
Jean-Pierre MartineauandClaude Fable 5 a23b12f6a4 Normalize UI copy: casing, headers, descriptions, backtracking
Enforce one voice across every screen: column and section headers are
UPPERCASE, menu items are Capitalized buttons, and short descriptions
drop their trailing period (with real ellipses instead of "...").

Tools gain a display `title` separate from their kebab-case CLI name,
so a menu item now reads exactly like the screen it opens ("Device
configuration" opens Device configuration) and result windows inherit
the same name. Every sub-screen backs out via a "Back" row below a
blank separator (the config editor was missing both; its Close is now
Back, spelling out the discard when changes are staged), while the main
menu and device picker keep "Quit". Panel and dialog titles now wear a
brighter tint of their border colour via new title.* theme styles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 22:51:42 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 99643f52d9 Overhaul the config editor into a modal-dialog experience
Rework the device-configuration screen from a flat list with an
advert-laden "danger zone" into one grouped menu whose every prompt is
a real floating modal popup over a persistent backdrop, matching the
quit dialog. The main menu now stays pushed on the session stack for
the whole editor session, so sub-prompts float over it rather than
replacing the screen.

Settings are staged and applied together (with a dirty-cancel
confirmation); device actions — adverts, reboot, backup/restore,
identity key, factory reset — run immediately behind their own
dialogs. Adverts get a dedicated menu (zero-hop, flood, or share this
node as a QR/URI contact card). Destructive actions gate behind a new
TypedConfirmDialog that requires typing RESET / IMPORT. Reboot hands
off to the reconnect dialog and waits for the companion to return.
Location can be set by pointing at the full-screen map (new
LocationPickScreen) or by typing coordinates.

Make every dialog consistent: a short title in the border, the
instruction as an in-body prompt, and a width sized to its content
instead of a fixed wide cap. TextScreen/SelectScreen/AutocompleteScreen
gain an optional in-body prompt, and those plus ConfirmScreen gain a
dialog_width; the prompt is threaded through the surface and session.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 22:28:42 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 62f19da405 Soft-retry unacked direct messages up to twice
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>
2026-07-09 21:41:14 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 19636e31e6 Float a fading ring spinner over laggy device navigations
Menu navigation over a slow link (Bluetooth especially, but serial too)
can sit on a blank frame while the companion answers — the menu screen is
popped and the tool's first prompt not yet pushed. Fill that gap with a
top-of-z-order spinner so the wait reads as work, not a hang.

- RingSpinner: a reusable, screen-agnostic braille circle with a cyan->blue
  comet chasing the rim. Plotted a quarter-arc and mirrored for exact 4-fold
  symmetry; stroked over a band of radii so it reads clearly as a circle.
- BusyOverlay + TuiSession.busy_overlay: draw the ring as the top-most float,
  centered, only in the gaps between screens (empty stack) so it never buries
  a prompt. It holds black for 200ms then fades in over 400ms, so quick work
  shows nothing and longer work never pops in. The fade restarts every time
  the ring is re-exposed (pop/reset), not just on first display.
- Wire it into menu navigation and the post-splash monitor resume for any
  real device link (mock simulator opts out); surfaced via ctx.ui.busy_overlay.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 21:33:43 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 12577a1538 Add "Unpair & quit" to drop a BLE bond on exit
The quit dialog grows a third button, shown only when the session is on a
Bluetooth link that Windows actually holds a bond for (never serial, an open
PIN-less companion, or a non-Windows platform). Choosing it disconnects the
link and then drops the OS-level pairing during teardown, so the next launch
re-runs the PIN ceremony. MeshTerm's remembered-device record is deliberately
kept — unpairing forgets the credential, not the device identity.

Backing this: static WinRT helpers is_ble_paired / unpair_ble mirroring
_pair_ble_windows (Windows-gated, best-effort), an active_address on the
context, and _can_unpair / _unpair_on_exit in the menu.

Also close the WinRT BluetoothLEDevice handles in every helper. Each
from_bluetooth_address_async returns an IClosable that pins the OS ACL link
open; unpair removes the bond but not the link, so a leaked handle left the
device "unpaired but still connected" and refusing to re-pair until it was
power-cycled. Closing the handle in a finally is what finally tears the link
down.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 20:46:02 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 0d2ffac70f Show the Bluetooth PIN as six fixed slots, capped at six digits
MeshCore pairing PINs are always six digits, so the PIN dialog now caps input
at six characters and draws a fixed six-slot field: each typed digit shows as a
bullet and each still-blank slot as a muted centre dot (· ), so the field reads
as a row of PIN boxes rather than a growing masked line. _LineEditor gains an
optional max_length (swallows keys past the cap, truncates an over-long paste)
and a slots render mode backing the field.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 20:00:46 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 7d40929341 Pair PIN-protected BLE companions via the WinRT ProvidePin ceremony
bleak's pair() on Windows only performs the CONFIRM_ONLY ("Just Works")
ceremony and never sends a passkey, so a PIN-protected MeshCore companion
bonds without authentication and then rejects the GATT subscribe on its
authenticated UART characteristic with Insufficient Authentication -- a
correct --ble-pin was reported as "rejected" and the device could never
connect.

MeshCoreDevice now runs the WinRT PROVIDE_PIN ceremony itself before opening
the link, creating the ENCRYPTION_AND_AUTHENTICATION bond the characteristic
requires; Windows persists it, so later reconnects need no PIN. A first GATT
auth failure with a supplied PIN also triggers one unpair-and-re-pair retry to
heal a stale unauthenticated bond from an older attempt. The step is a
best-effort no-op off Windows, when no PIN is set, or when already bonded.

Verified on hardware (AA:BB:CC:DD:EE:FF, MeshCore-Homestead): correct PIN
pairs and connects (reads model Seeed Tracker T1000-E), wrong PIN fails clean
with the "rejected" DeviceAuthenticationError, no PIN reports "requires a PIN",
and a bonded device reconnects with no PIN. Full CLI path exercised end to end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 19:58:31 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 588ee67c3b Prompt for a Bluetooth PIN in the picker; bail cleanly in the CLI
When a companion needs pairing, the interactive picker now opens a centered PIN
popup (its own bordered box under the wordmark, matching the splash) and retries the
connection with the entered code — re-asking with a "rejected" note on a wrong PIN,
and returning to the device list on Esc. The masked field clears its stale error as
soon as the user types a fresh code.

The connect failure is now a distinct DeviceAuthenticationError (a DeviceCommandError
subclass), so the picker can catch "needs a PIN" specifically while the scripted CLI
still catches the base class, prints the remedy (pass --ble-pin), and bails without a
prompt — no interactivity for scripted runs. --ble-pin already feeds the first attempt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 19:39:07 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 77390860da Handle PIN-protected Bluetooth companions gracefully
A BLE companion that requires pairing accepts the link-layer connection but rejects
the GATT subscribe with "Insufficient Authentication", which bleak raised as a bare
BleakGATTProtocolError — escaping connect() as an unhandled traceback.

Translate that (and related "Insufficient Encryption"/"not paired" rejections) into
a clean DeviceCommandError that names the fix: pass --ble-pin, and pair once in the
OS Bluetooth settings on Windows. It's classified as its own actionable case, never
as a dropped link, so the session offers a PIN rather than a pointless reconnect.

The startup probe now lets this actionable error through instead of collapsing every
failure to a bare "didn't answer", so the picker shows the real remedy in the splash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 19:26:31 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 d4fa056526 Identify companion hardware by its firmware model
Query MeshCore's device-info frame (send_device_query) on connect to learn the
board's own model string (e.g. "Seeed Tracker T1000-E") — the only reliable source,
since BLE companions advertise no manufacturer data, use randomized addresses with
no IEEE OUI, and expose no GATT Device Information Service.

The probe folds the model into the identity dict, so it's captured during the
connection that already happens, then remembered in the device store (preserved
across reconnects like the node name). The device list and splash picker rename
their "Vendor" column to "Hardware": it shows the firmware model for confirmed
devices and falls back to the USB vendor name for merely-attached serial ports.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 19:17:43 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 6d51b4e2ab Add a TYPE column and name known devices on the picker
Reworks the startup device picker so it reads honestly at a glance:

- VENDOR is now just the hardware maker. A BLE advert rarely carries one
  (confirmed against real hardware: the companion advertises no
  manufacturer_data), so it stays blank instead of mislabelling the
  transport "Bluetooth" as a vendor.
- A new TYPE column marks the transport with an icon: a plug emoji for
  wired serial, and the Bjarkan rune the Bluetooth logo is drawn from,
  rendered white on Bluetooth blue and flanked by half-block slivers so
  it reads as a slim rounded badge.
- Every confirmed companion (not just the most-recent one) is shown by
  the mesh node name learned at connect time, so a known COM11 reads as
  its node name rather than the OS's generic "USB Serial Device".
- Confirmed companions sort to the top, most-recently-used first, and
  wear their name in white so they stand out from merely-detected ports.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 18:59:54 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 7850587e2a Add Bluetooth LE companion support alongside serial
Companions can now be reached over Bluetooth as well as USB. Rather than
a parallel BLE path, this generalizes the existing serial abstractions so
a device is identified by a transport (serial | ble) plus a target (COM
port or Bluetooth address); every feature rides the same Device interface
and works over either link unchanged.

- discovery: DiscoveredDevice gains transport/address/name and
  transport-aware stable_id/label/confidence/target; discover_ble_devices
  scans for MeshCore* adverts via bleak, discover_all merges both.
- connection: MeshCoreDevice opens create_ble vs create_serial by
  transport; new link_present() liveness (OS port enumeration for serial,
  the BLE client's is_connected flag for Bluetooth); is_connection_lost
  recognizes Bleak errors and BLE transport callbacks; probe_device is the
  transport-aware smoke test.
- disconnect: the session watcher polls transport-neutral link_alive(), so
  a dropped BLE link raises the same reconnect dialog and auto-resume as an
  unplugged cable; reconnect rebuilds the same endpoint by address.
- selection/context/store/profiles: --ble address, remembered BLE default,
  and BLE device profiles all resolve to the right transport.
- UI/CLI: startup picker scans both transports behind a spinner and tags
  Bluetooth companions; `devices` lists them (--no-ble to skip); global
  --ble/--ble-pin flags; README and example config updated.

Validated on hardware: discovery, connect, reconnect, and channel
messaging (send + receive) over BLE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 18:44:39 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 6decc11c65 Restyle the info page to match the nodes list
Give the device-config table the same frameless SIMPLE_HEAD look as the
nodes list: left-justified accent title, muted header rule, no borders.
Indent each setting under its section heading, drop the redundant
(key) suffixes, and tighten the descriptions (no trailing periods).
Collapse the adv_loc_policy labels to off/on so the value cell no longer
duplicates the description.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 03:37:10 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 8e59f41886 Recolour the wordmark with the node-list heat gradient
Regenerate logo.ansi from logo.txt so the block rows fade white (top) to
red (bottom) along the node list's recency ramp, with the box-drawing
shadow lines in a dark slate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 03:02:08 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 030c333450 Refine the node table: pinned header, steady sort, legend, and colours
Keep the Name/Heard/Pkts/Key labels pinned to the top row once the rows
scroll past them, and break metric ties by name ascending so equal-heard
nodes no longer flip between sort directions. Mute the packet count to
match the Heard column, and drop placeholder dashes to a darker gray so
they recede below the real values. Always list every node type in the
legend instead of only those currently present.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 02:55:20 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 6cfe435d55 Redesign the nodes page with heat-map recency and live sorting
Sort contacts alphabetically with our own node pinned first, and add
Heard (relative age) and Pkts (overheard-packet) columns alongside a
per-type glyph and the full path-hash-highlighted key. Drop the boxed
table for a clean header rule and a glyph legend.

Colour each name by a continuous log-scale heat map of how recently it
was heard (white -> yellow -> orange -> red -> grey), and give the
node-type icons a shared, map-safe palette across the whole app: a
yellow star for us, a loud pink node and a calmer violet repeater
(both in hues no basemap uses, so nodes pop more than repeaters),
a white room and an orange sensor.

In the menu the list becomes an interactive screen: left/right pick the
sort column (cyan header + direction triangle), up/down set ascending/
descending, with column widths reserved so toggling never shifts the
row. The scripted CLI keeps a one-shot render via --sort.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 02:38:28 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 76202ec0d1 Hide the devices inventory from the menu, keep it on the CLI
By the time the interactive menu is up a companion is already selected,
so the read-only devices listing has no job there; it belongs on the
command line as a startup-time "which port is my radio?" diagnostic.

Add a menu_visible flag to the Tool contract (skipped by the menu loop,
never by CLI registration) and set it False on the devices tool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 00:23:26 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 dd097e48e5 Prompt to reconnect or abort when the device drops mid-session
Watch the companion link while the menu runs; if it disconnects, float a
centered dialog with a spinner and a single Abort button while a
background task retries the connection. The dialog dismisses itself the
moment the device returns, so the user's only action is to give up:
Enter aborts, and Esc is deliberately inert so a stray keypress can't
drop the session while a replug may be a second away.

Give every popup dialog the same cyan reverse-highlight button. This
adds a "selected" theme style (and "err.reverse"): Rich silently renders
a style string that mixes a theme name with an attribute ("reverse
brand") as plain text, so the reverse is baked into a single-name theme
entry instead. Center the button chips with manual padding rather than
Text(justify="center"), which strips a styled span's trailing whitespace
and left the label hugging the fill's left edge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 00:07:45 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 b02e3b9f4b Always render the map in braille, dropping block mode
The block-element fallback halved the vertical resolution and looked
poor, so remove the braille/block toggle entirely. Drops the `t` key,
the persisted map_block preference, and the quadrant-glyph rendering
path; the map now always draws 2x4 braille dots.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 23:18:51 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 e0487b46e6 Confirm quitting with a centered Cancel/Quit dialog
Buttons read Cancel then Quit, with Quit highlighted so Enter commits it
and Esc cancels. The question is orange, the selected button cyan, and the
idle button gray, so the current choice is unmistakable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 22:41:05 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 bfae10b387 Repaint the map's edge instead of the whole frame, and remember the mode
The braille base map leans on glyphs a terminal font may lack; the terminal
draws each missing glyph as a double-width fallback, which shoves the row and
smears the panel's right padding and border. prompt_toolkit's differential
paint models those cells as single-width, so its picture of the screen diverges
from the terminal and it never rewrites the smear.

The previous fix forced a full-frame repaint every paint while in braille mode,
which the 1s header-refresh tick turned into a visible periodic flicker. Replace
it with a targeted scrub: after a move, force just the two right-edge columns
(padding + border) to repaint by poking a sentinel into pt's cached frame. The
map body redraws wholesale as it pans, so its interior self-heals; only the
static edge needed help. Block mode can't smear, so it scrubs nothing.

Also:
- Persist the braille/block mode (app_state.map_block) so the map reopens the
  way it was left; braille stays the default.
- Blank the frame on exit (drop pt's cached frame once) so the menu drawn
  underneath doesn't inherit leftover braille garbage.
- Reword the t hint to "toggle braille/block".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 22:20:09 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 ebac6c3a4f Confirm quitting with a vertical leave dialog
Choosing "quit" or pressing Esc at the top menu asked to leave through a
horizontal Yes/No toggle that sat at odds with the rest of the TUI, which is
select lists everywhere else. Replace it with a small select dialog in that
same style: "Quit and disconnect" is the highlighted default so Enter leaves,
while Esc — or choosing "Stay" — returns to the menu.

select() grows optional filterable/footer_hint passthroughs so this short,
fixed choice skips type-to-filter and shows a tailored "Enter quit · Esc stay"
hint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 21:45:27 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 6b84281d0b Render the map so any terminal font can show it
The braille base map and its labels lean on Unicode a terminal font may not
have: 8-dot braille (U+2840-28FF) that many fonts omit, and place/node names
in scripts or emoji outside a Latin monospace font. A missing glyph renders as
double-width tofu, which both looks wrong and shoves the panel right border.

- Prefer a romanized name (name:latin/name:en) over the local script, and strip
  labels to single-cell characters, so foreign names and emoji node names can no
  longer tofu or knock the grid out of alignment.
- Add a block-element base-map mode, toggled with t: 2x2 quadrant blocks instead
  of 2x4 braille - half the vertical resolution, but glyphs essentially every
  monospace font has. Braille stays the default.
- Zoom with PageUp/PageDown as well as +/-.
- While braille mode is active, force a full repaint each paint so the
  double-width-fallback corruption on the right border is scrubbed on every move.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 21:09:18 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 bdbe5cce30 Navigate the TUI like a text editor
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>
2026-07-08 20:30:06 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 8906e4ed53 Enliven the conversation list and keep section headings in view
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>
2026-07-08 20:03:31 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 aa537d5503 Force process exit if teardown wedges on quit
The app sometimes hung on exit. The device/async teardown is clean; the
culprit is prompt_toolkit's Windows input reader, which runs on a
non-daemon executor thread blocked in a Win32 wait. In a rare teardown
race that thread stays blocked and the interpreter's atexit thread joins
hang the process after cleanup is already done.

Arm a daemon watchdog once the user commits to quitting: if a clean exit
doesn't complete within a grace period it flushes and os._exit(0)s. A
daemon thread still runs while the main thread is stuck in that join, so
it always terminates. The happy path is untouched (the process is gone
before the timer fires) and committed DB writes are durable regardless.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 19:35:25 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 5b7fe854cb Hang wrapped chat lines and color @mentions
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>
2026-07-08 19:03:33 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 fb3c7a15e2 Spin the delivery glyph while a direct message awaits its ack
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>
2026-07-08 18:32:29 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 101b48bc71 Adopt the probed device and polish the startup splash
Rework the startup device picker into a self-contained splash flow:

- probe_meshcore (renamed from smoke_test_meshcore) now leaves the confirmed
  connection open and returns it, so AppContext.adopt_device reuses that live
  radio instead of reopening (many boards reset on each serial open). Restore
  the library's normal per-command timeout once the handshake succeeds.
- Extract the Braille animation into a reusable tui.Spinner; BusyScreen now
  delegates to it.
- Pin the wordmark to a fixed vertical anchor so it never shifts as the box
  swaps between the device list, spinner, and messages.
- Esc or a trailing Quit row leaves the splash and exits the program.
- Show the copyright only on the opening splash; retire it once a device is
  chosen, even across a failed smoke test and re-pick.
- Disable prompt_toolkit's loop exception handler and route asyncio (and the
  awaited spinner ticker) errors to the file log, so a stray background-task
  error can't dump a traceback over the full-screen UI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 18:09:27 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 b692cce463 Confirm devices by smoke test on the startup splash
Reserve the "MeshCore device" tag for hardware we have actually talked to,
rather than guessing from the USB vendor ID:

- DeviceStore now keeps a registry of every confirmed device (keyed by
  stable serial id), remembered forever, migrating the old single-record
  file transparently.
- Selecting a device on the splash runs an immediate smoke test
  (smoke_test_meshcore): a genuine companion is remembered and confirmed;
  anything else re-prompts with a message to choose another. The probe uses
  a short handshake window so a non-MeshCore port is rejected cleanly in a
  few seconds instead of blocking on meshcore's full default timeout.
- The probe renders in place on the chromeless splash via a new BusyScreen
  (animated Braille spinner) driven by session.busy_startup, so the wordmark
  and box stay put instead of a separate progress dialog.
- Silence the meshcore library's console logging (its import calls
  logging.basicConfig, which printed "INFO:meshcore:..." over the TUI):
  route it to the JSON-lines file log with a NullHandler guard on root.
- The devices tool now marks confirmed devices and stops asserting "LoRa"
  before a device is proven.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 17:52:30 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 e52e5daf87 Draw the splash wordmark from a colored logo.ansi file
Replace the baked-in block-glyph constant with a pre-coloured logo.ansi read
fresh on every render, so the wordmark can be re-styled by editing that file
alone. It uses the ANSI Shadow face with green letters (#4ade80) and a
single-line dark-green bevel (#166534).

The banner path in compose_startup now centers already-coloured ANSI rows (as
one left-aligned block) rather than force-styling them a single colour, and a
new muted, centered footnote sits below the box — the splash uses it for a
"© <year> Homestead" copyright notice. The plain-text source is kept as
logo.txt for regenerating the art, and the old ansilogo.txt is removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 17:32:38 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 b66d1f7626 Redraw the startup device picker as a centered splash
The companion-device picker no longer borrows the in-menu frame. It is now a
chromeless splash: the MeshTerm wordmark, centered, above a box sized to the
device list rather than stretched across the terminal, with no header/footer
status bars.

A new `chrome` flag on Screen routes a base screen to `frame.compose_startup`,
which draws the banner and centers a content-sized panel. The device rows are
laid out in aligned columns (name / port / vendor / tag) under a muted header,
and type-to-filter is disabled (new `filterable` flag on SelectScreen) since the
list is short and fixed. The no-devices case gets the same splash treatment.

The wordmark lives in a code constant (meshterm/ui/logo.py, generated from
ansilogo.txt) so it survives installation regardless of packaged data files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 23:19:47 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 97557fc9b8 Frame the map's dense node core by default, tunable with --fraction
The default view fitted every located node, so a few distant outliers
(here a node near Toronto, 500+ km from the Montréal core) zoomed the
whole mesh out to a continental scale. Frame the densest half instead —
the nodes nearest the median centre — so the map opens on a useful local
region; the rest simply fall off the edges. Verified against a live
159-node mesh: 0.5 lands on the Greater Montréal metro (~77 km across,
two-thirds of nodes visible) rather than eastern Canada.

Viewport.fit gains a `fraction` argument (default 1.0, unchanged for
existing callers); the map's opening view and `r` reset pass 0.5, and
`meshterm map --fraction/-f` exposes the knob on the CLI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 22:03:26 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 61f4eff8dc Drop the Skip row from the device picker
The picker now lists only real discovered devices. Cancelling with Esc
still falls back to --mock / configure-later, so no behavior is lost.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 21:13:57 -04:00