Found in a bug audit of the just-merged friends-list feature. No crashes
or security holes, but five real state/correctness bugs:
- Host child dying on its own left the share campaign running, so it kept
pushing a now-dead ticket to friends (retrying offline ones forever) and
leaked share_status/met/share_code. The unexpected-exit path now captures
the stderr error, then routes through the full stop_host() teardown
(notably stop_share). (gui/mod.rs pump_host_events)
- on_friend_request downgraded an already-Accepted friend back to
PendingIncoming when they re-sent a request (e.g. after losing their
store). It now stays Accepted and re-confirms. (friends.rs)
- on_friend_accept advanced *any* known peer to Accepted, including a
PendingIncoming one — a peer could mark itself accepted without the local
user's consent. Now only a PendingOutgoing request we sent is honoured.
(friends.rs)
- A ShareCode redelivered by an ACK-loss retry fired a duplicate desktop
notification. push_notice now reports whether the code is new/changed and
only then toasts. (gui/mod.rs)
- An inbound control message could be delayed up to IO_TIMEOUT on a degraded
link because handle() awaited the sender's close before forwarding it.
Forward to the UI first, then await close so the ACK still flushes.
(control.rs)
Adds two friends-store transition tests (accept ignores a pending-incoming
peer; request doesn't downgrade an accepted friend). 47 gui / 8 headless
tests pass, clippy + fmt clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The host's "auto-share my code with" picker was session-scoped — an
in-memory exclusion set that reset to share-with-all on every launch.
Move the preference onto the friend itself: a `share: bool` on `Friend`
in friends.toml, `#[serde(default = true)]` so new friends are included
and an older file without the field loads as share-with-all. The picker
now toggles the stored flag and persists immediately (like the other
settings), and `selected_share_targets` filters on it. This drops the
parallel `share_excluded` state and is self-cleaning: removing a friend
takes their preference with them, no stale ids linger.
`upsert`'s update path leaves `share` untouched, so a name/presence
refresh can't reset the user's choice.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Build the friends feature on top of the phase-2 control plane: you can
now befriend someone you've connected with and manage a contacts list.
- common/friends.rs: a persisted FriendStore in its own friends.toml
(kept out of config.toml so a headless --reconfigure can't clobber it,
same as identity.key). Friends are keyed by stable control EndpointId;
state is PendingOutgoing / PendingIncoming / Accepted. The handshake
transitions (on_friend_request → mutual-match detection, on_friend_
accept) are pure and unit-tested.
- gui/code.rs: the bootstrap. The GUI host wraps its share code as
`pixelpassF1:<control-id>.<ticket>` so a viewer learns the host's
stable id; unwrap is lenient, so a bare/CLI ticket still works (no
friend offer). The video/streaming path is untouched.
- presence service gains an outbound path (unbounded channel → per-msg
send tasks) and exposes our control id for wrapping codes.
- gui wiring: on connect, the viewer announces itself to the host with a
Hello (carrying our display name); the host replies once, so both ends
learn each other and an "Add friend" offer appears on the running
host/view screens. Incoming requests/accepts/declines fold into the
store with desktop notifications. New Friends screen (accept/decline/
remove, edit your display name, see your id) reachable from the menu,
which shows a pending-request count. New [gui] display_name setting,
seeded from $USER.
Verified: friends store + handshake transitions covered by unit tests
(7); code wrap/unwrap round-trips (4); the control loopback still passes;
the live GUI starts clean with the presence endpoint online. fmt +
clippy clean on both features; 41 gui + 8 headless tests pass. The full
two-party UX (connect → mutual add → persisted) wants a cross-machine
manual check, as usual.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>