Skip to content

Improvement wave 3: 27 verified fixes across sessions, presence, AI, a11y, settings, and the updater - #80

Merged
scotej merged 32 commits into
mainfrom
feat/improvements-wave3
Jul 25, 2026
Merged

Improvement wave 3: 27 verified fixes across sessions, presence, AI, a11y, settings, and the updater#80
scotej merged 32 commits into
mainfrom
feat/improvements-wave3

Conversation

@scotej

@scotej scotej commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Improvement wave 3 — 27 verified fixes across sessions, presence, AI, a11y, settings, and the updater

The third improvement wave, run with the established process (survey → adversarial per-finding verification → one focused commit per item → multi-lens adversarial review of the branch). 31 commits; all local gates green (build, lint, test 826 passing, check-tokens, check-strings, check-contrast, format:check, cargo fmt --check, build-storybook, check-a11y 286 axe checks).

Please merge with a merge commit, not squash (PR #43 / #75 precedent) so the per-commit rationale survives.

How this was scoped

A 12-subsystem survey produced 51 candidates; adversarial verification kept 40 and refuted 11 (e.g. a DB-mutex-poisoning recovery, a presence-emit dedupe, and closing the stale #77 issue were all knocked down with cited reasoning). Of the 40, this branch lands the 27 that are correct, in-scope, and safe to verify by reading on this dev box; the rest are deferred below with cause.

What landed

Correctness (Sev2)

  • Home tail remount (I51): an unkeyed fragment rendered at a different child index per view branch, so React re-mounted the always-on presence/inbox room on every view switch — re-firing the I49 goodbye flicker, blanking the friends list for up to a heartbeat, and dropping an invite that arrived in the gap. Fixed with a stable <Fragment key>.
  • OS-sleep study minutes (I52): total_minutes was pure wall-clock, so a session slept on persisted the whole overnight span as study time (a free streak day + a ~10-hour bar). Now min(wall, monotonic); not retroactive.

Sessions & presence (Sev3)

  • Deliberate peer Leave ends the session at once instead of a 20 s "reconnect" wait + a Rejoin into a dead room, while a genuine blip — including one mid-session where another peer joins/leaves — still gets the grace window (I53).
  • A friend's first arrival after launch now notifies (I54); pending-invite countdown is honest on first render (I55 clock); session-hello display name is capped + bidi-sanitized like every other untrusted-name path (I55 name).

AI (Sev3)

  • A dead webcam reports once and skips ticks instead of a toast every few seconds (I56).

Accessibility (Sev3)

  • Focus ring now clears WCAG 1.4.11 in both themes — it was ~2.6:1 dark / ~1.8:1 light and the gate missed it by measuring the opaque accent, not the 40%-alpha ring; the gate now measures the real alpha (I57). Dropdown/menu rows get a highlight that actually paints (was bg-raised on bg-raised, 1.00:1) (I58). The session-log and notes scrollers are keyboard-reachable (I59).

Settings & updater (Sev3)

  • Settings-search keywords route to the panes that own each setting (I60); "Reset shortcuts" survives a combo collision (I61); Settings → About no longer offers a mid-session Restart/Check and stops claiming "you're on the latest" before any check ran (I62).

Added / quality-of-life

  • A failed 24-word restore names the words that aren't in the wordlist (I63); the focus-over-time tooltip shows the day (I64); the stats CSV leads with the headline tiles (I65); the corrupt-DB recovery dialog points at the friends-backup import (I69).

Rust backend (Sev3–4) — reviewed by reading; CI is the first compiler on the dev box

  • sidecar_start opens the log before spawning, so an open failure can't orphan a multi-GB llama-server (I66); the respawn budget counts consecutive short-lived deaths instead of a sliding window that could respawn forever (I67); the cross-session insights read is narrowed to AI-distraction rows, ~4× less IPC, with the Tauri command name unchanged (I68).

Tests & CI

  • Pinned regression vectors for the six rendezvous topic/password derivations (a typo there silently strands auto-updated friends in different rooms with no error path) — I re-derived all six from the primitives to confirm the pins; signed-hello peerId↔pubkey gate coverage; inbox replay-guard coverage; and a release job that blocks publishing a draft whose latest.json is missing a platform (I70).

Docs — DESIGN-SYSTEM §4 inventory true-up, ARCHITECTURE §11/§12, README versioning.

Review pass

A 6-lens adversarial review (Rust compile-correctness, session, friends/AI, a11y/tokens, updater/settings/stats, tests/docs) confirmed 3 low-severity defects in this wave's own commits, all fixed in e55d664 with tests:

  1. The deliberate-leave gate used a single boolean where departures are explained per-peer, so an intervening join by another peer could strand a still-absent blipper → now a per-peer Set.
  2. The friend-online settle window reused the 60 s heartbeat window, so a slow-connecting already-online friend could re-read as an arrival → dedicated NOTIFY_SETTLE_MS (3 min).
  3. The hello name cap was 64 bytes vs the inputs' 64 UTF-16 units, truncating legitimate CJK/emoji names → raised to 192 bytes.

The Rust, a11y, and updater/settings lenses returned clean.

Deferred, with cause (not dropped)

  • waitForVideoReady degrade — needs a desktop smoke test (same reason as I50); can't run headless.
  • Keychain↔identity.json mismatch probe, monitor-clamped window restore, release global shortcuts during a keybind capture — all add new Rust surface, and this box can't compile the Tauri crate, so they need CI-first landing in a dedicated pass rather than bundling into a broad sweep.

Manual tests still owed (can't run headless here)

  • Second-peer walk: open Settings / start a session and confirm no presence flicker or phantom "came online" on a friend's screen (I51); confirm a friend's Leave ends your session immediately with no Rejoin, and that a blip + mid-session join/leave still waits (I53).
  • Desktop walk: park the app an hour, send yourself an invite, confirm the row opens at "Expires in 5 min" (I55 clock); focus-ring visibility in light theme (I57); keyboard scroll of the session log/notes (I59).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Recovery errors now identify unknown backup words.
    • Stats CSV exports include headline summary metrics.
    • Focus chart tooltips show dates.
    • Sessions end promptly when peers deliberately leave.
    • Update checks and restarts are blocked during active sessions.
  • Bug Fixes

    • Improved session timing after sleep, presence notifications, invite countdowns, webcam error messages, shortcut resets, and database recovery guidance.
    • Enhanced keyboard navigation and visible focus indicators.
    • Improved protection against duplicate invitations and unstable application restarts.
  • Documentation

    • Updated architecture, versioning, design-system, issue tracking, and unreleased changelog documentation.

scotej added 30 commits July 24, 2026 12:40
…w switches

The `tail` block (InboxBoot, PairDeepLinkBoot, ContactImportDialog,
TopicGateModal) is defined once but rendered at a different child index in
each branch — index 1 on main, index 2 in settings/report/session. React
matches unkeyed children by position, so on main -> settings, settings ->
main, main -> session and report -> main it compared the tail fragment
against `<Settings/>` / `<Report/>`, found different types, and deleted and
rebuilt the whole subtree.

That remount ran InboxBoot's cleanup: `presence.leave()` broadcasts a
`{leaving:true}` goodbye (friends' dots blip offline and N3 fires a spurious
"came online" ping — the I49 symptom via a path the friend-list diff can't
defend), the rebuilt presence map starts empty so Invite buttons disappear
until the next sweep, and an invite landing in the inbox teardown window is
dropped outright since Nostr doesn't buffer.

`key="app-tail"` pins the slot, so the fiber is reused across every branch.
Session -> report and the in-session settings overlay were already preserved
by index realignment; those two are unaffected. Also corrects the now-stale
"view switches re-mount the boot" note in pairDeepLink.ts — `launchConsumed`
stays, it still guards the identity/onboarding mount.

Wave item: home-tail-and-invite-clock (04).
…on-empty

`now` was seeded once at mount and only ever advanced by the 10s interval,
which is armed by the same `pending.length` change that puts the first row on
screen — so its first tick lands 10s AFTER the row is already visible. With
close-to-tray on by default the app sits parked for hours, so a 5-minute
envelope rendered as "Expires in 214 min" for those 10 seconds. That is
precisely the window the arrival toast and OS notification summon the user to
look in, and the countdown is the first thing they read on the row.

Reseeding after the empty guard keeps it off the mount path and off the n -> 0
path; the accept path still re-validates expiry at click time, so this only
ever affected the displayed number.

The set-state-in-effect suppression follows the existing ones in this repo
(Report, SessionsCategory, Onboarding): the reseed can't move into an
adjust-during-render because `Date.now()` trips react-hooks/purity.

Not covered by a test — vitest is node-env with no jsdom/RTL and the defect
lives in the container's useState/useEffect wiring; the store test is
unaffected. Storybook is unaffected too, since PendingInvitesView takes `now`
as a controlled prop.

Wave item: home-tail-and-invite-clock (06).
Closing the lid on a live session and ending it in the morning persisted
the whole overnight span: wall-clock subtraction alone turned a 45-minute
session into a ~600-minute bar, a fabricated streak day, and a report
reading "Studied for 612 min".

host/join now capture a monotonic origin alongside startedAt and the
leave handler persists min(wall, monotonic). performance.now() advances
on demand, so a hidden or throttled webview (the default close-to-tray
flow) still reads the real awake span, and min() can only shrink an
inflated number -- where a platform's monotonic clock does include
suspend this degrades to today's value rather than under-counting real
study time. The live footer clock applies the same rule from the store's
startedAtMono so it does not read 612:34 on wake while the row says 45.

Historical inflated rows are not corrected. The crash-orphan adoption
path in the Rust writer is left alone: it derives its span from audit
events, which stop while suspended.

Item: session-minutes-monotonic
Improvements wave items 01 + 02 (same mechanism). When a friend clicks
Leave they broadcast a signed 'left' audit event and await it before
room.leave(), but nothing consumed it: the receiver armed the 20 s S1
grace window, sat on "Waiting for your friend to reconnect…" for someone
who was never coming back, and then offered "Rejoin session" into a room
nobody is in. In a two-person session whoever does not click Leave first
hit this at the end of essentially every session.

SessionView now marks the peerId when a verified 'left' lands (reliable
because both messages ride the same ordered data channel), and
wireSessionRoom skips the grace window only when EVERY departure since
the last join was marked — a mixed "one leaves, one blips" three-way
still waits, which is the safe way to be wrong. The end is staged as a
new 'peer' reason so the Report suppresses Rejoin without overloading
'user'. peerJoined clears the mark, because trystero's peerId is
process-stable and a re-invited peer would otherwise have their next
genuine blip kill the session instantly.

Crash / kill / tray-quit never emit 'left', so those keep today's grace
window and the 'auto' + Rejoin path.
The N3 "friend came online" baseline was unbounded: the first time we
resolved a friend ONLINE since subscribing never notified, so it could not
tell "already online when we subscribed" from "offline then walked in hours
later". With the app parked in the tray all day, that silently dropped the
one event the feature exists for.

Item 05 (friend-online-first-arrival). Keeps the baseline set (it is the
only sweep guard that is independent of tick ordering) and bounds it with a
per-friend watch-start map: subscribe time for the seed set, add time for
anyone imported later, so a ContactCard import stays silent for its own
settle window. Past ONLINE_WINDOW_MS (60s, the same window isOnline uses) a
first online resolution is a real arrival. Strictly additive — everything
that notified before still notifies. The decision moves to a pure
shouldNotifyFriendOnline helper so it can be covered node-env.
The session hello was the one untrusted-name surface the PR-29/I37 sweep
skipped: validateHelloPayload returned display_name verbatim, so bidi
overrides and zero-width chars a peer pasted into their own name rendered
as-is on every peer tile, audit row and note. normalizeUntrustedName now
takes an optional byte bound and the hello passes HELLO_NAME_CAP = 64,
mirroring the 64-char maxLength of our own name inputs rather than the
32-byte card NAME_CAP, so no legitimate name is truncated.

Normalization applies strictly to the returned value; the signed canonical
bytes and the sender side are untouched, so the wire format is unchanged
and old peers still verify. The local self-tile stays unnormalized by
design, so a user who pastes a bidi char into their own name still sees it
reversed on their own tile while peers see it clean. Item 35.
Item ai-capture-error-latch. When a camera dies mid-session (unplugged,
OS-revoked, or grabbed by another app on Windows) getFaceTrack() keeps
returning the ended track, every tick throws CaptureError('track_ended'),
and SessionView's undeduped toast.error fires every 5-30 s for the rest
of the session — on top of the MediaErrorBanner that already says the
same thing, and in violation of the "callbacks fire at most once per loop
lifetime" contract the options block documents.

The face-track guard now also skips when readyState is 'ended', matching
the screen-track guard directly below it; both cases are "input absent",
so the tick reschedules without counting a sample (scoring is unchanged —
applyJudgment already sat inside the try that was throwing). onCaptureError
is latched behind state.captureErrorReported and cleared on the next
successful tick, mirroring sidecarErrorReported / batteryNoticeShown.

Follow-up worth tracking: the repeated toast was also the only signal for a
persistent *screen*-capture failure, whose handler sets no AI runtime status.
That path is now quieter than before. Fixing it properly needs a paired
recovery callback (the onBatteryPause/onBatteryResume shape), so it is left
out of this change.
… real

Items 13 + 38 — the same token surface.

The global focus ring is `focus-visible:ring-3 focus-visible:ring-accent-ring`
and, because `:focus-visible { outline: none }` removes the UA default, it is
the only focus affordance on most primitives. At 40% alpha it composited to
2.50-2.56:1 dark and 1.76-1.86:1 light — below SC 1.4.11's 3:1 floor and
effectively invisible on the light canvas. Raised to 60% dark (#F2B05A99) and
80% light (#8C5215CC), which measures 4.24/4.12/3.90/4.29 dark and
3.85/4.02/3.67/3.44 light over base/surface/raised/sunk. Both `tokens.ts` and
`index.css` are edited — they are hand-mirrored with no parity guard.

The ring's INNER edge on a `variant="default"` Button falls to 2.31:1 against
the amber fill (it was 3.83:1 at 40%). That is a deliberate trade: an amber
ring cannot win both edges, and the outer edge against the canvas is the one
carrying the identification load. The alternative, `ring-offset-*` across 16
call sites, is a layout change and out of scope.

check-contrast never saw any of this: the pairing registered the fully OPAQUE
accent as the foreground and reported 9.81:1 dark / 5.85:1 light. `evaluate()`
strips foreground alpha by design, so an alpha'd `fg` would be silently
ignored too; instead the ring now sits in the bg stack, where `compositeBg`
honors alpha, with the surface as foreground (contrast is symmetric). One
pairing becomes four, one per surface. `accent-default on bg-base` stays
covered by `text-accent-default on bg-base`, so coverage does not regress.
Both stale "≥4.7:1" comments that leaned on the vacuous number are corrected.

Item 38: `shadow.glow` said 3px in tokens.ts but 4px in index.css and
DESIGN-SYSTEM. tokens.ts is the one that was right — it matches the 15 shipped
`ring-3` sites — so the two stale copies come down to 3px rather than the
reverse. No rendered pixel changes; `--shadow-glow` has zero consumers. §11 now
names the mechanism the primitives actually use.

Not machine-walked: this box cannot run the Tauri app, and neither axe-core nor
any test evaluates focus-indicator contrast. Worth an eyeball in Storybook.
Item 14. DropdownMenuItem and DropdownMenuRadioItem declared their
highlighted state as focus:bg-bg-raised on a DropdownMenuContent that is
already bg-bg-raised — 1.00:1 in both themes, so the mid-session mic and
speaker pickers gave a keyboard user no indication of which row was
highlighted while arrowing through them, and mouse hover was equally dead.
With two identically-named devices (the exact case the radio semantics were
added to disambiguate) the user was arrowing blind. Items carry
outline-hidden and the global :focus-visible reset removes the UA outline,
so no fallback indicator existed.

Replaces the dead fill with the house ring-3 at ring-inset (the content is
overflow-clipped) in accent-default, which the existing "accent-default
border on bg-raised" pairing already pins at 5.44:1 light / 8.21:1 dark.
focus:text-text-primary stays — it is what the check-contrast
IGNORED_COOCCURRENCES entry for this file is keyed on. focus:, not
focus-visible:, because Radix moves DOM focus on pointer-move and this is
the hover affordance too. The destructive row keeps a red ring to match its
tint, mirroring button.tsx.

Not eyeballed live: this box cannot run the Tauri app. Worth a human look at
both device pickers in both themes to confirm ring-3 does not read heavy on
2-5 short rows; ring-2 is the fallback if it does.
Item 14's sibling instance, kept separate from the dropdown change. The
`secondary` variants read `bg-bg-raised text-text-primary
hover:bg-bg-raised` — the hover fill is byte-identical to the idle fill, so
20+ in-app secondary buttons (Report, Dashboard, every Settings category,
SessionView, ModelPicker) had no hover affordance at all. Same string in
badge.tsx for the anchor-badge case.

hover:bg-bg-surface is the one-token fix and moves in both themes: surface
is darker than raised in dark, lighter in light. check-contrast covers the
six new token co-occurrences with existing pairings; no PAIRINGS edit
needed.

Hover pixels are not eyeballed live — this box cannot run the Tauri app.
Item 15. AuditLogPanel's viewport and SessionNotesPanel's capped list are
both bare scrolling divs whose only children are text rows — no button, no
link, nothing focusable. The notes form is a sibling of its list, not a
descendant. So both regions were pointer-only, and the read-back
AuditLogPanel's own doc comment advertises ("preserving scroll position on
read-back") was unavailable to a keyboard (WCAG 2.1.1). Before this,
`tabIndex` appeared exactly once in the whole frontend, and it was a -1.

Both get tabIndex={0} and the house focus ring, at ring-inset because a
normal ring is clipped by the pane's own overflow box. They land together
on purpose: fixing only the audit log puts a tab stop above the notes list
but not on it.

Scope note: this is a real macOS fix and a no-op on Windows — WebView2 has
auto-focused childless scrollers since Chromium 127, WKWebView has not.

Both panels gained an Overflowing story, because the shipped ones barely
scroll and axe's scrollable-region-focusable rule is inapplicable without
real overflow. Measured in the built Storybook under Playwright chromium:
audit 1124 vs 435, notes 291 vs 180. `npm run build-storybook &&
npm run check-a11y` was run before and after — 283 tests green at baseline
(the predicted `listitem` fallout does not reproduce) and 285 green now.
…e settings

The v1.6.0 settings search shipped four keywords in the `advanced` bucket for
settings that live in other panes: 'tray'/'minimize' (Notifications),
'capture displays' (AI), and 'auto-update' (About). Typing any of them — or
pressing Enter, which selects filtered[0] — opened Advanced, where the setting
does not exist, reading as if it were removed. Meanwhile Advanced's own rows
(launch-at-login, data folder, share log, replay onboarding, clear history)
contributed no keywords, so those queries returned "No settings match."

Move each term to the pane that owns the concept and give Advanced keywords for
its real rows. 'capture displays' routes to the AI pane, whose row renders only
when AI is on — correct, and consistent with the other AI keywords behind the
same gate. Removing 'auto-update' from Advanced also fixes "update", which was
Enter-routing to Advanced instead of About; 'share log' (not bare 'diagnostics')
keeps the shared "diagnostics" query on Network as before.

Adds a compile-time `Record<SettingsCategoryId, readonly string[]>` guard in
Settings.tsx over strings.settings.searchKeywords so a future pane without a
keyword bucket fails tsc, instead of importing the id union into the leaf
strings module (which would invert the existing strings -> feature dependency
direction). Item 21.
…lision

resetShortcutsToDefaults reset ptt-friends then ptt-ai in a fixed order and
awaited each. If the user had rebound PTT-AI onto the friends-default combo
(reachable through the rebind UI), the first register hit an already-registered
combo, global-hotkey threw AlreadyRegistered, and the second reset never ran —
ShortcutsCategory swallowed the rejection into console.error and the store's
`error` field is rendered nowhere, so the button was a visual no-op.

Reset the action squatting on the other's default combo first so that combo is
free before re-registering it, and wrap each call in try/catch so a refusal on
one binding still lets the other reset; keep and rethrow the last error.
ShortcutsCategory now surfaces it via toast.error with new copy in strings.ts
(settings.shortcuts.reset.resetError) — the residual fully-swapped case, which
no reset order can break, is a real reject rather than a silent nothing.

Adds a stateful keybindings.test.ts fake that rejects a register whose combo
the other action currently holds: asserts the reorder lands both bindings on
their defaults in the single-collision case, and that the second setter still
runs when the first rejects. Per the item spec the Rust register-side skip is
deliberately dropped — it would re-open the #47 B5 tray-idle combo swallow.
Item 22.
…e banner

Item 25. Settings → About is reachable mid-session (#47 B2), but its
"Restart now" and "Check now" affordances had no session check, unlike
UpdateReadyBanner which suppresses itself while a session is live. Restart
mid-session is an unconfirmed quit that bypasses leaveBeforeQuit and the
CloseRequested confirm, silently losing the session; a mid-session check
pulls an installer onto the live WebRTC mesh.

Enforce in the store where the node-env test harness reaches it:
installAndRestart bails false when isSessionActive, and checkNow drops the
now-false userInitiated exemption so every check defers during a session.
AboutCategory disables both buttons when a session is active and states the
reason in the help line (no info by color alone), following the shipped
ModelPicker actionsLocked idiom. Flips the updater-store test that pinned
the old exemption and adds an installAndRestart session-block case.
…heck ran

Item 26. AboutCategory's help text used a bare else that caught both idle
(the store's initial value, held for the first ~20s of every launch and
after a session-deferral reset) and a background-check-failure error state,
asserting "You're on X, the latest." with no check behind it — worst exactly
when the update channel has silently died.

Makes the help state-driven with upToDate as an explicit positive branch:
idle and background failures now read step-agnostic unknown / retry copy
instead of a false currency claim. The store's tested background-silence
contract is untouched — this is a UI-local ternary plus three strings on a
pull surface. Also relabels the status row so it no longer duplicates the
read-only Version row.
classifyMnemonic collapsed a misread word and a checksum-only slip into
one "Those 24 words don't add up" message, pointing at all 24 words
equally on the highest-stakes screen in the app. Split the 'invalid' case
into its actionable half: a module-level BIP39 Set in recoverLogic.ts
(not lib/crypto, whose cross-version contract stays untouched; not
features/friends' isBip39Word, which would invert an existing feature
edge into a cycle) fills unknownWords on the 24-word path. The 'invalid'
error now names up to 3 non-wordlist tokens (plural-aware, capped, reads
correctly at the 24-token comma-paste case) and falls back to the old
copy for a pure checksum failure. Words are held in Recover state beside
error and cleared on edit so a later checksum-only failure never renders
stale words. Adds a UnknownWord story for axe coverage and extends
recoverLogic tests. Item: bip39-unknown-words.
Item 18. The trend line plots one dot per AI-scored session on a bare
1,2,3… ordinal axis and its tooltip showed only "73% focused", so a dip
was un-anchorable to a day while the study-minutes bar chart stacked
directly above it carries dates. Carry startedAt into the datum and add a
day line to TrendTooltip, using the shared dayKey formatter so the string
is byte-identical to the bar chart's. Axis stays dataKey="index" — the
help copy already says the x-axis is session order.
Item 20. The Stats pane leads with the current streak and average score,
but the CSV export wrote only the trailing-30-day daily minutes and
per-partner counts — neither headline number, and the streak is not
derivable from daily totals (it needs a single >=25-min session per day).
Prepend a `summary` section (total sessions, streak, average score,
scored sessions) to buildStatsCsvModel; a null average writes '' to keep
the "AI off" vs "scored 0" distinction. No header or signature change, so
the section-filtered assertions still pass; added a summary-section test.
Item 10. open_log_file ran after spawn_llama, so a failed log open (disk
full, an AV/indexer holding the file, a permissions change on the data dir)
returned Err while the freshly spawned multi-GB CommandChild was dropped
without kill() and before guard.child was set. CommandChild does not kill on
drop and kill_blocking then finds no child, so the orphan outlives the app.
Reordering rotate -> open -> spawn front-loads the last fallible step so no
failure can sit between the spawn and guard.child = Some(child).
Item 12. The watcher's budget was a 30s sliding window keyed on crash
timestamps, so any death more than RESTART_WINDOW after the previous one reset
the counter to 1. A model that reliably OOMs ~20s into loading therefore
respawned forever: `errored` was never set, so AiCategory's "AI model crashed /
Restart" row never appeared and the user got a silently dead AI. Switch the
discriminator to child uptime: consecutive respawns that each died before
MIN_HEALTHY_UPTIME (120s) accumulate toward RESTART_BUDGET, while a child that
ran at least that long resets the streak, so a sidecar that self-heals after a
long clean run is never starved. Extract next_attempts as a pure fn and pin its
three branches, since the Tauri crate can't compile on the dev box.
…mport

Item 29. After a corrupt app.db is set aside and recreated, DB_RECOVERED_BODY
told the user "you'll need to pair with your friends again" — wrong, because
the encrypted friends backup imports from the OS keychain identity, which the
DB wipe never touches (only app.db and its -journal/-wal/-shm sidecars are
removed). Name the actual control (Settings -> Identity -> Import friends) so a
user who exported a .svfriends file restores the whole list in two clicks, and
so the dialog stops asserting an out-of-band re-pairing flow is required.
The Stats focus-insights view only ever consumes ai_warning/ai_alert rows
carrying `reasoning`, yet the cross-session read shipped every audit row
(joined/left/pomodoro_*/topic_*/break_*) over IPC to be parsed and discarded.
Add `WHERE kind IN ('ai_warning', 'ai_alert')` so the read scales with what
the pane displays rather than lifetime history (~4x smaller IPC payload).

Rename the internal db fn `list_all` -> `list_ai_distractions_all` so the
narrowed contract is legible; the `audit_events_list_all` Tauri command name
is kept stable to leave the IPC surface untouched. A comment at the WHERE
clause links the kind list to its TS twins (isDistraction / reportData) since
a new distraction kind added in TS alone would silently vanish here.

Per the verifier-corrected item 19: query-narrowing only — no migration, no
indexes (they do nothing for the WHERE-less scan and only cost an irreversible
MAX_KNOWN_VERSION bump). Tests and ARCHITECTURE.md updated to match.
…lish it

Item 27. release.yml runs both platform legs with fail-fast off, and publish
is the last job, so a draft can survive with only the platform whose leg built
(the other tripped on fetch-llama-server, a clippy break, a runner timeout).
Publishing that half-built draft strands every friend on the missing platform:
the plugin's check() rejects a latest.json whose platforms map has no matching
key. Nothing else asserted the published manifest was complete.

Adds a verify-updater-manifest job that fails hard only when latest.json is
missing darwin-aarch64 or windows-x86_64, and stamps the draft's own title
"INCOMPLETE, DO NOT PUBLISH" where the publish button is, since a red Actions
run already trains no one. contents: write is scoped to the job (a draft is
readable and editable only with push access, so contents: read would 404).
if: !cancelled() plus a no-release branch keeps "both legs died before any
draft existed" from misreporting as a missing platform; the success path
clears the title back so a fixed re-run is publishable; and gh transport
errors stay non-blocking so a chronically-red gate can't teach the reviewer
to publish past it.
The six peer-binding topic/password derivations (inbox, pair, presence) plus
buildPairAuthMessage are exercised only symmetrically by the integration
suites, so an in-place change to a label, payload encoding, or joiner passes
every gate yet strands peers on an older build after an auto-update. Pin the
computed bytes so such an edit fails loud. sessionTopic is pinned for
collision hygiene only and documented as transmitted, not a rendezvous
contract. Item 33.
validateHelloPayload is the sole gate binding a trystero peerId to an Ed25519
pubkey (audit attribution, sessions.peer_pubkeys, note verification, tile
names), yet every branch was exercised only indirectly. Cover the round-trip,
the peer_id-mismatch impersonation gate, a foreign-pubkey claim, tampered
name/joined_at, and the malformed-shape rejections, plus a pinned
canonical-bytes assertion on serializeHelloForSig so a key-order edit — which
would silently strand already-installed builds — fails loud. Item 32.
…t delivery

The PR-18/I30 (from_ed_pubkey, nonce) dedup in subscribeToOwnInbox is
load-bearing on the everyday path since #47 C1: the inbox races Nostr +
MQTT and the guard is the sole idempotency mechanism absorbing one
envelope delivered twice, yet nothing asserted it. The existing
serialization test sends the same envelope twice but only awaits a
promise (resolves once regardless), so it cannot catch a dedup
regression.

Add two cases to tests/integration/invite.test.ts, both delivering
directly on the mocked inbox room via joinTopic so the concurrent
window is real (sendInviteEnvelope serializes per inbox topic and never
exercises it):

- one envelope delivered twice concurrently fires onValidInvite once —
  catches the guard being removed AND an await slipping between the
  has/set pair, the exact regression the concurrent shape exists to
  find;
- two distinct-nonce invites from the same friend both dispatch —
  catches an over-broad key that drops the nonce.

Verified each bites by injecting the corresponding regression into
inbox.ts. Covers items 07 and 34 (replay-guard ground); no ledger row —
I30 is already marked fixed, this only adds its missing test.
… story-coverage claim

§4's component inventory was the untouched V1 build spec and had drifted
hard from the real tree; five code comments defer to it by name.

Primitives table: add the two files it omitted (Checkbox, Skeleton) so it
matches src/components/ui/ (23 files, 21 rows). Skeleton's row cites its
existing §6/§10 "no spinners, use this instead" prose.

Composed table: regenerate from `ls src/components/*.tsx` (31 files). Drop
the `BenchmarkRunner` ghost (never built — grep finds it only in this doc)
and fold its shipped behavior into a note that benchmark progress renders
inline on the model-picker card; drop `FriendRow`, which is a file-local
row renderer inside features/friends, not a src/components export. Move the
five feature-owned surfaces that were mislabeled as src/components
(FriendsList, AddFriendDialog, ModelPicker, FocusInsights,
IdentityLoadErrorView) into one sentence pointing at src/features/*, which
obeys the same Radix wall tree-wide and is exhibited in Storybook. Add the
thirteen shipped components the table never listed.

Rule 4 claimed "adding a component without a story fails the PR check" —
no such gate exists (no check-stories script, ever). Rewrite it to describe
what runs: check-a11y over the stories that exist in CI, with coverage a
review responsibility. Preserves the V1-P2 provenance tag.

Items 16, 40, 17 (doc half).
…ities inventory

§11 still listed the retired `system_fetch_latest_version` command (removed in
v1.5.0 when X6's tauri-plugin-updater subsumed it). Replace it with an annotated
pointer that also records why the `version_check_enabled` store key is still read
(never written) — deleting the bullet would strand that key looking like dead
code and invite a real opt-out-carry-over regression. Item 36.

§12 named three plugins the ACL does not grant (shell / global-shortcut /
autostart) and omitted the ones it does (dialog / deep-link / updater), and the
repo-layout tree showed a one-file capabilities dir. Transcribe the real
default.json and ai-dialog.json inventories from the files, add the missing
ai-dialog.json tree entry, and state the Rust-only-plugins rule instead of a
stale three-item list. Item 37.
The release-line sentence asserted "v1.5.0 is the current release" while
HEAD ships 1.6.0 — the same drift I45 fixed once at v1.3.1 and that has
recurred three times, because the one-click release-prep flow only stages
the five version files and never touches README. Rewrite the chain so every
entry is past-tense provenance and CHANGELOG.md owns "whatever shipped most
recently"; this also finally names v1.6.0's settings search and startup-perf
work, which appeared nowhere in README. Naming v1.6.0 does not obligate a
v1.7.0 append — the sentence no longer claims which release is current, so it
cannot go stale again. Item 39 (docs-readme-versioning); durable rewrite only,
no CLAUDE.md checklist change.
Three low-severity defects the review pass surfaced in this wave's own
commits, all fixed with tests:

- session end-at-once (606f583): the "every peer left deliberately" gate
  used a single `sawUnexplainedLeave` boolean, but a departure is explained
  per-peer via `departedPeerIds`. An intervening join by a *different* peer
  reset the flag, so a peer still absent from an earlier unexplained blip
  was forgotten and the room could end instantly with no grace/Rejoin.
  Track unexplained-absent peers in a Set, cleared per-peer on rejoin.

- friend-online notify (df030f2): the first-arrival settle window reused the
  60s presence-heartbeat window, so a friend who was online all along but
  whose presence handshake resolved >60s after launch pinged as an arrival —
  the boot-sweep false positive the baseline exists to prevent. Give the
  decision its own NOTIFY_SETTLE_MS (3 min), sized above realistic
  connection-establishment latency.

- session hello name cap (c3d8b1d): HELLO_NAME_CAP was 64 *bytes* but our
  own inputs cap at 64 UTF-16 *units*, so a legitimate multibyte name (CJK,
  emoji) was silently truncated. Raise to 192 bytes — the worst case for a
  64-unit input — and fix the comment that equated the two.
Record the improvement-wave-3 fixes: an Unreleased CHANGELOG section in
the app's voice, and one ISSUES.md row per confirmed item (plus a note on
the survey→verify→review process and the test-/doc-only outcomes that
aren't ledgered).
Copilot AI review requested due to automatic review settings July 24, 2026 05:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@scotej, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 33 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c6a63c49-4f9d-47b9-af08-aa2bec6d0ac0

📥 Commits

Reviewing files that changed from the base of the PR and between 0737077 and ad4599b.

📒 Files selected for processing (5)
  • ARCHITECTURE.md
  • DESIGN-SYSTEM.md
  • src-tauri/src/lib.rs
  • src/design/index.css
  • src/strings.ts
📝 Walkthrough

Walkthrough

The PR adds release-manifest verification and updates session lifecycle, sidecar restart, audit querying, accessibility, friend notifications, identity recovery, settings, updater, statistics, and documentation behavior, with corresponding unit, integration, and Storybook coverage.

Changes

Release, documentation, and platform behavior

Layer / File(s) Summary
Release verification and project documentation
.github/workflows/release.yml, ARCHITECTURE.md, CHANGELOG.md, DESIGN-SYSTEM.md, ISSUES.md, README.md
Adds draft updater-manifest verification and documents updated architecture, accessibility, release history, and maintenance findings.
Session lifecycle and backend data handling
src/features/session/*, src/stores/sessionStore.ts, src-tauri/src/commands/*, src-tauri/src/db/*, tests/unit/session-*, tests/unit/topics.test.ts
Adds monotonic session timing, deliberate departure attribution, narrowed AI audit queries, revised sidecar restart streaks, and related validation.
AI capture and friend presence behavior
src/features/ai/*, src/features/friends/*, src/routes/Home.tsx, tests/integration/invite.test.ts, tests/unit/ai-sample-loop.test.ts, tests/unit/friend-online-notify.test.ts
Deduplicates capture errors, skips ended tracks, bounds friend-online notification suppression, refreshes invite countdowns, and preserves mounted home bootstrap elements.
Mnemonic validation and recovery feedback
src/features/identity/*, src/strings.ts, src/stories/Recover.stories.tsx, tests/unit/recoverLogic.test.ts
Identifies unknown recovery words and displays targeted invalid-mnemonic messages.
Settings search, shortcut reset, and updater locking
src/features/settings/*, src/stores/settingsStore.ts, src/features/updater/updaterStore.ts, src/strings.ts, tests/unit/keybindings.test.ts, tests/unit/updater-store.test.ts
Adds typed search-keyword coverage, collision-tolerant shortcut resets, toast errors, and active-session updater safeguards.
Stats chart and CSV output
src/features/stats/*, tests/unit/file-export.test.ts
Adds dates to trend tooltips and headline summary rows to stats CSV exports.
Accessible focus styling and component validation
src/components/*, src/components/ui/*, src/design/*, scripts/check-contrast.ts, src/stories/*
Makes scrollable panels keyboard-focusable, updates focus and hover styling, revises design tokens and contrast pairings, and adds overflow stories.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.62% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the PR’s main theme and scope: a broad wave of verified fixes across sessions, presence, AI, accessibility, settings, and updater.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/improvements-wave3

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/release.yml:
- Around line 241-247: Update the release completeness check around the gh
release download flow to list release assets first and distinguish an absent
latest.json from genuine API or download failures. Fail the job and apply the
existing stamping behavior when the manifest is missing, while retaining the
non-blocking warning-and-exit path only for transport or API errors.

In `@ISSUES.md`:
- Around line 11-12: Update the Improvement wave 3 landing plan in ISSUES.md to
specify the repository-required squash merge strategy, replacing the
merge-commit objective while preserving the surrounding survey and fix history.

In `@README.md`:
- Around line 381-383: Update the v1.6.0 entry in the README release summary to
restore the missing verb before “a searchable settings rail,” while preserving
the surrounding release descriptions and formatting.

In `@src/features/session/SessionView.tsx`:
- Around line 725-732: The departure handling in SessionView’s verified left
branch incorrectly assumes the auditAction.send() completion guarantees peer
receipt before onPeerLeave. Replace this ordering dependency with a
request/response acknowledgment, or invoke markPeerDeparted(peerId) through the
same teardown path that emits the leave notification, ensuring deliberate
departures always take the immediate peer path.

In `@src/stories/AuditLogPanel.stories.tsx`:
- Line 76: Replace the hard-coded 480px height on the AuditLogPanel story’s
wrapper div with the existing layout design token or shared Storybook viewport
fixture, while preserving the current overflow-testing behavior and
flex/background classes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: aa88fea0-3c8b-41d9-bcf0-00a7bfb68840

📥 Commits

Reviewing files that changed from the base of the PR and between eb74005 and 0737077.

📒 Files selected for processing (57)
  • .github/workflows/release.yml
  • ARCHITECTURE.md
  • CHANGELOG.md
  • DESIGN-SYSTEM.md
  • ISSUES.md
  • README.md
  • scripts/check-contrast.ts
  • src-tauri/src/commands/sessions.rs
  • src-tauri/src/commands/sidecar.rs
  • src-tauri/src/db/audit_events.rs
  • src-tauri/src/lib.rs
  • src/components/AuditLogPanel.tsx
  • src/components/ui/badge.tsx
  • src/components/ui/button.tsx
  • src/components/ui/dropdown-menu.tsx
  • src/design/index.css
  • src/design/tokens.ts
  • src/features/ai/sampleLoop.ts
  • src/features/friends/InboxBoot.tsx
  • src/features/friends/PendingInvites.tsx
  • src/features/friends/contactCard.ts
  • src/features/friends/friendOnlineNotify.ts
  • src/features/friends/pairDeepLink.ts
  • src/features/identity/Recover.tsx
  • src/features/identity/RecoverView.tsx
  • src/features/identity/recoverLogic.ts
  • src/features/session/SessionNotesPanel.tsx
  • src/features/session/SessionView.tsx
  • src/features/session/hello.ts
  • src/features/session/host.ts
  • src/features/session/join.ts
  • src/features/session/lifecycle.ts
  • src/features/settings/Settings.tsx
  • src/features/settings/categories/AboutCategory.tsx
  • src/features/settings/categories/ShortcutsCategory.tsx
  • src/features/stats/FocusInsights.tsx
  • src/features/stats/statsData.ts
  • src/features/updater/updaterStore.ts
  • src/routes/Home.tsx
  • src/stores/sessionStore.ts
  • src/stores/settingsStore.ts
  • src/stories/AuditLogPanel.stories.tsx
  • src/stories/Recover.stories.tsx
  • src/stories/SessionNotesPanel.stories.tsx
  • src/strings.ts
  • tests/integration/invite.test.ts
  • tests/unit/ai-sample-loop.test.ts
  • tests/unit/file-export.test.ts
  • tests/unit/friend-online-notify.test.ts
  • tests/unit/keybindings.test.ts
  • tests/unit/recoverLogic.test.ts
  • tests/unit/session-end-reason.test.ts
  • tests/unit/session-grace.test.ts
  • tests/unit/session-hello.test.ts
  • tests/unit/session-reentry-merge.test.ts
  • tests/unit/topics.test.ts
  • tests/unit/updater-store.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: Frontend
  • GitHub Check: Rust (Windows)
🧰 Additional context used
📓 Path-based instructions (14)
src/**/*.{ts,tsx,css}

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.{ts,tsx,css}: Use design tokens instead of hard-coded visual values in frontend TypeScript, TSX, and CSS files.
Meet WCAG AA requirements for every text/background pairing in both themes; do not convey information by color alone.
Respect reduced-motion preferences through the global kill switch; new motion sites must be gated by default.

Files:

  • src/features/session/host.ts
  • src/features/settings/categories/ShortcutsCategory.tsx
  • src/components/ui/badge.tsx
  • src/features/session/join.ts
  • src/features/session/SessionNotesPanel.tsx
  • src/stories/Recover.stories.tsx
  • src/components/ui/button.tsx
  • src/stories/AuditLogPanel.stories.tsx
  • src/stories/SessionNotesPanel.stories.tsx
  • src/features/friends/contactCard.ts
  • src/features/friends/pairDeepLink.ts
  • src/design/tokens.ts
  • src/components/AuditLogPanel.tsx
  • src/routes/Home.tsx
  • src/features/friends/PendingInvites.tsx
  • src/stores/settingsStore.ts
  • src/features/updater/updaterStore.ts
  • src/design/index.css
  • src/features/settings/categories/AboutCategory.tsx
  • src/features/settings/Settings.tsx
  • src/features/session/hello.ts
  • src/components/ui/dropdown-menu.tsx
  • src/features/identity/recoverLogic.ts
  • src/features/identity/Recover.tsx
  • src/features/stats/statsData.ts
  • src/features/stats/FocusInsights.tsx
  • src/features/identity/RecoverView.tsx
  • src/features/session/SessionView.tsx
  • src/stores/sessionStore.ts
  • src/features/session/lifecycle.ts
  • src/features/ai/sampleLoop.ts
  • src/strings.ts
  • src/features/friends/InboxBoot.tsx
  • src/features/friends/friendOnlineNotify.ts
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.{ts,tsx}: Keep JSX text and aria-label literals consistent with the voice defined in DESIGN-SYSTEM.md §14, and prefer centralized strings.
Treat peer wire formats and identity derivation as cross-version contracts; coordinate changes so older peers and existing stored data remain compatible.
Do not add telemetry; the application must remain local-only.

Files:

  • src/features/session/host.ts
  • src/features/settings/categories/ShortcutsCategory.tsx
  • src/components/ui/badge.tsx
  • src/features/session/join.ts
  • src/features/session/SessionNotesPanel.tsx
  • src/stories/Recover.stories.tsx
  • src/components/ui/button.tsx
  • src/stories/AuditLogPanel.stories.tsx
  • src/stories/SessionNotesPanel.stories.tsx
  • src/features/friends/contactCard.ts
  • src/features/friends/pairDeepLink.ts
  • src/design/tokens.ts
  • src/components/AuditLogPanel.tsx
  • src/routes/Home.tsx
  • src/features/friends/PendingInvites.tsx
  • src/stores/settingsStore.ts
  • src/features/updater/updaterStore.ts
  • src/features/settings/categories/AboutCategory.tsx
  • src/features/settings/Settings.tsx
  • src/features/session/hello.ts
  • src/components/ui/dropdown-menu.tsx
  • src/features/identity/recoverLogic.ts
  • src/features/identity/Recover.tsx
  • src/features/stats/statsData.ts
  • src/features/stats/FocusInsights.tsx
  • src/features/identity/RecoverView.tsx
  • src/features/session/SessionView.tsx
  • src/stores/sessionStore.ts
  • src/features/session/lifecycle.ts
  • src/features/ai/sampleLoop.ts
  • src/strings.ts
  • src/features/friends/InboxBoot.tsx
  • src/features/friends/friendOnlineNotify.ts
src/**/*.{ts,tsx,rs}

📄 CodeRabbit inference engine (CLAUDE.md)

Never instruct users to paste a model file or BIP39 mnemonic into an AI service or chat.

Files:

  • src/features/session/host.ts
  • src/features/settings/categories/ShortcutsCategory.tsx
  • src/components/ui/badge.tsx
  • src/features/session/join.ts
  • src/features/session/SessionNotesPanel.tsx
  • src/stories/Recover.stories.tsx
  • src/components/ui/button.tsx
  • src/stories/AuditLogPanel.stories.tsx
  • src/stories/SessionNotesPanel.stories.tsx
  • src/features/friends/contactCard.ts
  • src/features/friends/pairDeepLink.ts
  • src/design/tokens.ts
  • src/components/AuditLogPanel.tsx
  • src/routes/Home.tsx
  • src/features/friends/PendingInvites.tsx
  • src/stores/settingsStore.ts
  • src/features/updater/updaterStore.ts
  • src/features/settings/categories/AboutCategory.tsx
  • src/features/settings/Settings.tsx
  • src/features/session/hello.ts
  • src/components/ui/dropdown-menu.tsx
  • src/features/identity/recoverLogic.ts
  • src/features/identity/Recover.tsx
  • src/features/stats/statsData.ts
  • src/features/stats/FocusInsights.tsx
  • src/features/identity/RecoverView.tsx
  • src/features/session/SessionView.tsx
  • src/stores/sessionStore.ts
  • src/features/session/lifecycle.ts
  • src/features/ai/sampleLoop.ts
  • src/strings.ts
  • src/features/friends/InboxBoot.tsx
  • src/features/friends/friendOnlineNotify.ts
**/*.{ts,tsx,rs}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,rs}: Add comments only when the reason is non-obvious; identifiers should carry meaning and code should read top-to-bottom.
Maintain scope discipline: do not refactor adjacent code during feature work or add abstractions for hypothetical future needs.

Files:

  • src/features/session/host.ts
  • src/features/settings/categories/ShortcutsCategory.tsx
  • src/components/ui/badge.tsx
  • tests/unit/friend-online-notify.test.ts
  • tests/unit/session-end-reason.test.ts
  • src/features/session/join.ts
  • src/features/session/SessionNotesPanel.tsx
  • src-tauri/src/commands/sessions.rs
  • src/stories/Recover.stories.tsx
  • src/components/ui/button.tsx
  • src/stories/AuditLogPanel.stories.tsx
  • src/stories/SessionNotesPanel.stories.tsx
  • src/features/friends/contactCard.ts
  • src/features/friends/pairDeepLink.ts
  • tests/unit/recoverLogic.test.ts
  • src/design/tokens.ts
  • src/components/AuditLogPanel.tsx
  • src/routes/Home.tsx
  • src-tauri/src/lib.rs
  • src/features/friends/PendingInvites.tsx
  • src/stores/settingsStore.ts
  • tests/unit/file-export.test.ts
  • src/features/updater/updaterStore.ts
  • src/features/settings/categories/AboutCategory.tsx
  • tests/unit/topics.test.ts
  • src/features/settings/Settings.tsx
  • tests/unit/session-hello.test.ts
  • src/features/session/hello.ts
  • src/components/ui/dropdown-menu.tsx
  • src/features/identity/recoverLogic.ts
  • src/features/identity/Recover.tsx
  • scripts/check-contrast.ts
  • src/features/stats/statsData.ts
  • src/features/stats/FocusInsights.tsx
  • src/features/identity/RecoverView.tsx
  • tests/unit/updater-store.test.ts
  • src/features/session/SessionView.tsx
  • tests/integration/invite.test.ts
  • tests/unit/session-reentry-merge.test.ts
  • src/stores/sessionStore.ts
  • src/features/session/lifecycle.ts
  • tests/unit/keybindings.test.ts
  • tests/unit/session-grace.test.ts
  • src/features/ai/sampleLoop.ts
  • tests/unit/ai-sample-loop.test.ts
  • src/strings.ts
  • src-tauri/src/commands/sidecar.rs
  • src-tauri/src/db/audit_events.rs
  • src/features/friends/InboxBoot.tsx
  • src/features/friends/friendOnlineNotify.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use TypeScript strict mode and ensure frontend code passes the TypeScript build and type checks.

Files:

  • src/features/session/host.ts
  • src/features/settings/categories/ShortcutsCategory.tsx
  • src/components/ui/badge.tsx
  • tests/unit/friend-online-notify.test.ts
  • tests/unit/session-end-reason.test.ts
  • src/features/session/join.ts
  • src/features/session/SessionNotesPanel.tsx
  • src/stories/Recover.stories.tsx
  • src/components/ui/button.tsx
  • src/stories/AuditLogPanel.stories.tsx
  • src/stories/SessionNotesPanel.stories.tsx
  • src/features/friends/contactCard.ts
  • src/features/friends/pairDeepLink.ts
  • tests/unit/recoverLogic.test.ts
  • src/design/tokens.ts
  • src/components/AuditLogPanel.tsx
  • src/routes/Home.tsx
  • src/features/friends/PendingInvites.tsx
  • src/stores/settingsStore.ts
  • tests/unit/file-export.test.ts
  • src/features/updater/updaterStore.ts
  • src/features/settings/categories/AboutCategory.tsx
  • tests/unit/topics.test.ts
  • src/features/settings/Settings.tsx
  • tests/unit/session-hello.test.ts
  • src/features/session/hello.ts
  • src/components/ui/dropdown-menu.tsx
  • src/features/identity/recoverLogic.ts
  • src/features/identity/Recover.tsx
  • scripts/check-contrast.ts
  • src/features/stats/statsData.ts
  • src/features/stats/FocusInsights.tsx
  • src/features/identity/RecoverView.tsx
  • tests/unit/updater-store.test.ts
  • src/features/session/SessionView.tsx
  • tests/integration/invite.test.ts
  • tests/unit/session-reentry-merge.test.ts
  • src/stores/sessionStore.ts
  • src/features/session/lifecycle.ts
  • tests/unit/keybindings.test.ts
  • tests/unit/session-grace.test.ts
  • src/features/ai/sampleLoop.ts
  • tests/unit/ai-sample-loop.test.ts
  • src/strings.ts
  • src/features/friends/InboxBoot.tsx
  • src/features/friends/friendOnlineNotify.ts
**/*

📄 CodeRabbit inference engine (CLAUDE.md)

**/*: Before committing or opening a PR, run the required build, lint, test, design-token, strings, contrast, accessibility, formatting, and relevant Rust quality gates.
Use one focused change per commit with a Conventional Commit subject such as feat:, fix:, chore:, docs:, or ci:; PRs should squash-merge.

Files:

  • src/features/session/host.ts
  • src/features/settings/categories/ShortcutsCategory.tsx
  • src/components/ui/badge.tsx
  • tests/unit/friend-online-notify.test.ts
  • tests/unit/session-end-reason.test.ts
  • src/features/session/join.ts
  • src/features/session/SessionNotesPanel.tsx
  • src-tauri/src/commands/sessions.rs
  • src/stories/Recover.stories.tsx
  • src/components/ui/button.tsx
  • src/stories/AuditLogPanel.stories.tsx
  • src/stories/SessionNotesPanel.stories.tsx
  • src/features/friends/contactCard.ts
  • src/features/friends/pairDeepLink.ts
  • tests/unit/recoverLogic.test.ts
  • src/design/tokens.ts
  • src/components/AuditLogPanel.tsx
  • src/routes/Home.tsx
  • src-tauri/src/lib.rs
  • src/features/friends/PendingInvites.tsx
  • src/stores/settingsStore.ts
  • tests/unit/file-export.test.ts
  • src/features/updater/updaterStore.ts
  • src/design/index.css
  • README.md
  • CHANGELOG.md
  • src/features/settings/categories/AboutCategory.tsx
  • tests/unit/topics.test.ts
  • src/features/settings/Settings.tsx
  • tests/unit/session-hello.test.ts
  • src/features/session/hello.ts
  • src/components/ui/dropdown-menu.tsx
  • src/features/identity/recoverLogic.ts
  • src/features/identity/Recover.tsx
  • scripts/check-contrast.ts
  • src/features/stats/statsData.ts
  • src/features/stats/FocusInsights.tsx
  • src/features/identity/RecoverView.tsx
  • tests/unit/updater-store.test.ts
  • src/features/session/SessionView.tsx
  • ISSUES.md
  • tests/integration/invite.test.ts
  • tests/unit/session-reentry-merge.test.ts
  • src/stores/sessionStore.ts
  • src/features/session/lifecycle.ts
  • tests/unit/keybindings.test.ts
  • tests/unit/session-grace.test.ts
  • src/features/ai/sampleLoop.ts
  • tests/unit/ai-sample-loop.test.ts
  • src/strings.ts
  • DESIGN-SYSTEM.md
  • src-tauri/src/commands/sidecar.rs
  • src-tauri/src/db/audit_events.rs
  • src/features/friends/InboxBoot.tsx
  • src/features/friends/friendOnlineNotify.ts
  • ARCHITECTURE.md
src/components/ui/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Import Radix and shadcn primitives only from src/components/ui/.

Files:

  • src/components/ui/badge.tsx
  • src/components/ui/button.tsx
  • src/components/ui/dropdown-menu.tsx
src/components/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Components under src/components/ should compose from ui/, src/design/, and shared utilities; reverse imports from ui/ are prohibited.

Files:

  • src/components/ui/badge.tsx
  • src/components/ui/button.tsx
  • src/components/AuditLogPanel.tsx
  • src/components/ui/dropdown-menu.tsx
**/*.test.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Write Vitest tests for applicable unit and integration behavior; component tests are not currently supported without an explicit test-harness scope expansion.

Files:

  • tests/unit/friend-online-notify.test.ts
  • tests/unit/session-end-reason.test.ts
  • tests/unit/recoverLogic.test.ts
  • tests/unit/file-export.test.ts
  • tests/unit/topics.test.ts
  • tests/unit/session-hello.test.ts
  • tests/unit/updater-store.test.ts
  • tests/integration/invite.test.ts
  • tests/unit/session-reentry-merge.test.ts
  • tests/unit/keybindings.test.ts
  • tests/unit/session-grace.test.ts
  • tests/unit/ai-sample-loop.test.ts
src-tauri/**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

src-tauri/**/*.rs: Preserve compatibility for Rust-side peer wire formats, identity derivation, and persisted data across manually installed updates.
For Rust changes, run cargo test, cargo fmt --check, and cargo clippy before committing or opening a PR.

Files:

  • src-tauri/src/commands/sessions.rs
  • src-tauri/src/lib.rs
  • src-tauri/src/commands/sidecar.rs
  • src-tauri/src/db/audit_events.rs
src/**/*.stories.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.stories.{ts,tsx}: Provide Storybook coverage for every primitive and feature component.
Ensure every Storybook story passes the axe-core accessibility gate.

Files:

  • src/stories/Recover.stories.tsx
  • src/stories/AuditLogPanel.stories.tsx
  • src/stories/SessionNotesPanel.stories.tsx
src/design/tokens.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Define every design token—colors, spacing, fonts, radii, shadows, motion, and z-index values—in src/design/tokens.ts; do not use raw hex values, arbitrary px, or inline cubic-bezier values elsewhere.

Files:

  • src/design/tokens.ts
.github/workflows/*.yml

📄 CodeRabbit inference engine (CLAUDE.md)

Use the documented release workflow or manual process to keep versions synchronized across package.json, package-lock.json, src-tauri/Cargo.toml, src-tauri/Cargo.lock, and src-tauri/tauri.conf.json.

Files:

  • .github/workflows/release.yml
src/strings.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Keep toast and notification copy in src/strings.ts; prefer this module for user-facing strings.

Files:

  • src/strings.ts
🪛 ast-grep (0.44.1)
tests/unit/file-export.test.ts

[warning] 141-141: Enforce overriding default config
Context: session({ id: 's1', score: 90, focused_pct: 0.7 })
Note: [CWE-1188] Insecure Default Initialization of Resource (default session cookie name not overridden).

(default-session-config-typescript)


[warning] 142-142: Enforce overriding default config
Context: session({ id: 's2', score: 70, focused_pct: 0.5 })
Note: [CWE-1188] Insecure Default Initialization of Resource (default session cookie name not overridden).

(default-session-config-typescript)


[warning] 143-143: Enforce overriding default config
Context: session({ id: 's3', score: null })
Note: [CWE-1188] Insecure Default Initialization of Resource (default session cookie name not overridden).

(default-session-config-typescript)

🪛 LanguageTool
CHANGELOG.md

[style] ~41-~41: The noun “invitation” is usually used instead of ‘invite’ in formal writing.
Context: ...ntom "came online" ping and dropping an invite that arrived in the gap. The connecti...

(AN_INVITE)


[style] ~48-~48: ‘in the meantime’ might be wordy. Consider a shorter alternative.
Context: ...g when another friend joins or leaves in the meantime. - **A friend's first arrival of the d...

(EN_WORDINESS_PREMIUM_IN_THE_MEANTIME)

🔇 Additional comments (62)
src/features/identity/Recover.tsx (1)

48-51: LGTM!

Also applies to: 66-66, 88-92, 130-130

src/features/identity/RecoverView.tsx (1)

22-25: LGTM!

Also applies to: 41-45, 55-57, 70-70, 207-207

src/features/identity/recoverLogic.ts (1)

1-20: LGTM!

Also applies to: 34-53

src/stories/Recover.stories.tsx (1)

54-62: LGTM!

tests/unit/recoverLogic.test.ts (1)

53-70: LGTM!

.github/workflows/release.yml (1)

13-15: LGTM!

Also applies to: 186-240, 249-263

ARCHITECTURE.md (1)

535-535: LGTM!

Also applies to: 560-561, 615-616, 639-645, 719-743

CHANGELOG.md (1)

21-105: LGTM!

DESIGN-SYSTEM.md (1)

54-54: LGTM!

Also applies to: 127-127, 212-212, 277-285, 298-326, 357-357, 528-528

ISSUES.md (1)

13-84: LGTM!

scripts/check-contrast.ts (1)

454-485: LGTM!

Also applies to: 495-496, 628-630

src/features/stats/FocusInsights.tsx (3)

26-26: LGTM!


158-162: LGTM!


208-220: 🎯 Functional Correctness

Verify tooltip dates use the stats timezone.

dayKey accepts an optional timeZone, but this call omits it. If the trend was bucketed using a user-selected timezone, timestamps near midnight can display the adjacent calendar date in the tooltip. Thread the stats timezone through the chart, or confirm that startedAt is already normalized to the runtime timezone.

src/features/stats/statsData.ts (2)

237-241: LGTM!


250-253: LGTM!

tests/unit/file-export.test.ts (2)

120-136: LGTM!


138-159: LGTM!

src/features/ai/sampleLoop.ts (1)

384-389: LGTM!

Also applies to: 431-431, 775-779, 876-876, 896-899

tests/unit/ai-sample-loop.test.ts (1)

152-157: LGTM!

Also applies to: 983-1025, 1027-1054

src/features/friends/InboxBoot.tsx (1)

17-20: LGTM!

Also applies to: 114-118, 129-140, 152-152, 166-181, 229-238

src/features/friends/friendOnlineNotify.ts (1)

3-8: LGTM!

Also applies to: 18-63

tests/unit/friend-online-notify.test.ts (1)

1-150: LGTM!

tests/integration/invite.test.ts (1)

124-136: LGTM!

Also applies to: 546-617

src/features/friends/PendingInvites.tsx (1)

117-123: LGTM!

src/features/friends/contactCard.ts (1)

199-205: LGTM!

src/features/friends/pairDeepLink.ts (1)

14-19: LGTM!

src/routes/Home.tsx (1)

10-20: LGTM!

Also applies to: 342-373

src/components/AuditLogPanel.tsx (1)

93-103: LGTM!

src/features/session/SessionNotesPanel.tsx (1)

60-66: LGTM!

src/components/ui/badge.tsx (1)

14-14: LGTM!

src/components/ui/button.tsx (1)

17-17: LGTM!

src/components/ui/dropdown-menu.tsx (1)

60-65: LGTM!

Also applies to: 81-81, 113-113

src/design/index.css (1)

23-23: LGTM!

Also applies to: 44-44, 93-93

src/design/tokens.ts (1)

32-37: LGTM!

Also applies to: 232-235

src/stories/AuditLogPanel.stories.tsx (1)

35-43: LGTM!

Also applies to: 73-75, 77-81

src/stories/SessionNotesPanel.stories.tsx (1)

54-69: LGTM!

src-tauri/src/lib.rs (1)

323-326: LGTM!

src-tauri/src/commands/sessions.rs (1)

114-119: LGTM!

src-tauri/src/db/audit_events.rs (1)

67-97: LGTM!

Also applies to: 182-249

src-tauri/src/commands/sidecar.rs (1)

488-497: LGTM!

Also applies to: 555-556, 665-680

tests/unit/session-hello.test.ts (1)

1-187: LGTM!

tests/unit/topics.test.ts (1)

1-124: LGTM!

src/features/settings/categories/ShortcutsCategory.tsx (1)

2-2: LGTM!

Also applies to: 67-79

src/stores/settingsStore.ts (1)

1073-1099: LGTM!

src/strings.ts (2)

229-243: LGTM!

Also applies to: 850-898, 1740-1760


1120-1121: 🎯 Functional Correctness | ⚡ Quick win

"Couldn't reset both shortcuts" overstates failure on partial success.

resetShortcutsToDefaults (settingsStore.ts) can fail on only one of the two setter calls while the other succeeds, yet still rethrows lastError — so this toast always says "both" failed even when one binding was actually reset to default.

✏️ Suggested wording tweak
-        resetError: (message: string) =>
-          `Couldn't reset both shortcuts: ${message}`,
+        resetError: (message: string) =>
+          `Couldn't finish resetting shortcuts: ${message}`,
tests/unit/keybindings.test.ts (2)

382-465: LGTM!


379-381: 🎯 Functional Correctness

No duplicate attempts declaration is present.

The current source has a single let attempts declaration here, so this does not break the TypeScript build.

			> Likely an incorrect or invalid review comment.
tests/unit/updater-store.test.ts (1)

203-217: LGTM!

Also applies to: 293-313

src/features/session/hello.ts (1)

11-26: LGTM!

Also applies to: 102-109

src/stores/sessionStore.ts (1)

17-22: LGTM!

Also applies to: 49-51, 77-79, 119-130, 157-157, 167-167, 176-176, 186-186, 206-206, 216-230, 250-255

src/features/session/lifecycle.ts (1)

177-180: LGTM!

Also applies to: 198-201, 224-241, 347-353, 410-410, 419-434

src/features/session/SessionView.tsx (1)

162-162: LGTM!

Also applies to: 1546-1546, 1731-1772

src/features/session/host.ts (1)

21-28: LGTM!

src/features/session/join.ts (1)

26-33: LGTM!

tests/unit/session-grace.test.ts (1)

177-286: LGTM!

tests/unit/session-end-reason.test.ts (1)

41-52: LGTM!

tests/unit/session-reentry-merge.test.ts (1)

61-159: LGTM!

src/features/settings/Settings.tsx (1)

58-141: LGTM!

src/features/settings/categories/AboutCategory.tsx (1)

18-18: LGTM!

Also applies to: 38-38, 88-97, 122-145

src/features/updater/updaterStore.ts (1)

137-141: LGTM!

Also applies to: 195-199

Comment on lines +241 to +247
# gh transport errors (or a wholly absent asset) must not redden the
# gate; only a genuinely missing platform key does.
if ! gh release download "$TAG" --pattern latest.json --clobber; then
echo "::warning::Could not fetch latest.json for '$TAG' (gh transport error or asset absent) — skipping the completeness check." \
| tee -a "$GITHUB_STEP_SUMMARY"
exit 0
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Fail when latest.json is absent.

An absent manifest follows the same success path as a transient gh failure, so an incomplete draft remains publishable and unstamped—the exact release state this job is meant to prevent. List assets first; fail/stamp if latest.json is missing, and keep only genuine API/download failures non-blocking.

Proposed fix
+          assets="$(gh release view "$TAG" --json assets --jq '.assets[].name')" || {
+            echo "::warning::Could not list release assets for '$TAG' — skipping the completeness check." \
+              | tee -a "$GITHUB_STEP_SUMMARY"
+            exit 0
+          }
+          if ! grep -Fxq latest.json <<<"$assets"; then
+            stamp_and_fail "Draft release '$TAG' has no latest.json. Do NOT publish — re-run the failed build leg."
+          fi
+
           if ! gh release download "$TAG" --pattern latest.json --clobber; then
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# gh transport errors (or a wholly absent asset) must not redden the
# gate; only a genuinely missing platform key does.
if ! gh release download "$TAG" --pattern latest.json --clobber; then
echo "::warning::Could not fetch latest.json for '$TAG' (gh transport error or asset absent) — skipping the completeness check." \
| tee -a "$GITHUB_STEP_SUMMARY"
exit 0
fi
# gh transport errors (or a wholly absent asset) must not redden the
# gate; only a genuinely missing platform key does.
assets="$(gh release view "$TAG" --json assets --jq '.assets[].name')" || {
echo "::warning::Could not list release assets for '$TAG' — skipping the completeness check." \
| tee -a "$GITHUB_STEP_SUMMARY"
exit 0
}
if ! grep -Fxq latest.json <<<"$assets"; then
stamp_and_fail "Draft release '$TAG' has no latest.json. Do NOT publish — re-run the failed build leg."
fi
if ! gh release download "$TAG" --pattern latest.json --clobber; then
echo "::warning::Could not fetch latest.json for '$TAG' (gh transport error or asset absent) — skipping the completeness check." \
| tee -a "$GITHUB_STEP_SUMMARY"
exit 0
fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 241 - 247, Update the release
completeness check around the gh release download flow to list release assets
first and distinguish an absent latest.json from genuine API or download
failures. Fail the job and apply the existing stamping behavior when the
manifest is missing, while retaining the non-blocking warning-and-exit path only
for transport or API errors.

Comment thread ISSUES.md
Comment on lines +11 to +12
**Improvement wave 3 (post-v1.6.0, `feat/improvements-wave3`).** A 12-subsystem multi-agent survey with per-finding adversarial verification, then a multi-lens review of the branch (the three low-severity findings it confirmed were fixed on-branch). Rows I51+ record the confirmed fixes; each shipped as its own commit with tests where the harness allows. Test-only and doc-only outcomes (rendezvous-derivation vectors, signed-hello gate coverage, inbox replay coverage, and the DESIGN-SYSTEM §4 / ARCHITECTURE §11–§12 / README true-ups) are not ledgered separately.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the required squash merge strategy.

The PR objective requests a merge commit; this repository requires PRs to squash-merge. Update the landing plan before merge.

As per coding guidelines, “PRs should squash-merge.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ISSUES.md` around lines 11 - 12, Update the Improvement wave 3 landing plan
in ISSUES.md to specify the repository-required squash merge strategy, replacing
the merge-commit objective while preserving the surrounding survey and fix
history.

Source: Coding guidelines

Comment thread README.md
Comment on lines +381 to +383
the verified backlog; **v1.5.0** brought in-app auto-update; **v1.6.0**
a searchable settings rail and a lighter, faster startup. `CHANGELOG.md`
has the full history, including whatever shipped most recently. The version

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Restore the missing verb in the v1.6.0 entry.

The sentence currently reads “v1.6.0 a searchable settings rail,” which is grammatically incomplete.

Proposed fix
- the verified backlog; **v1.5.0** brought in-app auto-update; **v1.6.0**
- a searchable settings rail and a lighter, faster startup.
+ the verified backlog; **v1.5.0** brought in-app auto-update; **v1.6.0**
+ brought a searchable settings rail and a lighter, faster startup.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
the verified backlog; **v1.5.0** brought in-app auto-update; **v1.6.0**
a searchable settings rail and a lighter, faster startup. `CHANGELOG.md`
has the full history, including whatever shipped most recently. The version
the verified backlog; **v1.5.0** brought in-app auto-update; **v1.6.0**
brought a searchable settings rail and a lighter, faster startup. `CHANGELOG.md`
has the full history, including whatever shipped most recently. The version
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 381 - 383, Update the v1.6.0 entry in the README
release summary to restore the missing verb before “a searchable settings rail,”
while preserving the surrounding release descriptions and formatting.

Comment on lines +725 to +732
// This mark is what lets the lifecycle layer tell a deliberate
// departure from a WiFi blip, and it is reliable only because
// handleLeave awaits this broadcast before room.leave() and both ride
// the same single ordered data channel — the mark always lands before
// trystero's own leave notification. Don't drop that await.
if (verified.kind === 'left') {
useSessionStore.getState().markPeerDeparted(peerId)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

trystero webrtc strategy makeAction send resolve semantics vs onPeerLeave connection close order

💡 Result:

In Trystero, the semantics of action sending and their relation to peer lifecycle events (like onPeerLeave) are defined by the library's decentralized, peer-to-peer nature [1][2]. Action Send Semantics The makeAction() method creates an action object that supports two modes: message actions and request actions [3][4]. 1. Message Actions: The send() method transmits data to one or more peers [3][5]. It returns a Promise that resolves once the local transmission (the process of handing the data off to the underlying WebRTC data channel) is complete [5][6]. It does not wait for a network-level acknowledgment from the recipient [3][5]. 2. Request Actions: When configured with kind: 'request', the request() and requestMany() methods implement request/response semantics [3][5]. These return a Promise that resolves with the actual response(s) provided by the peer(s), or rejects if the request times out [3][5]. Connection Close and onPeerLeave Trystero operates on a decentralized architecture where peer connections are established individually [1][2]. There is no central server, so "leaving a room" is the aggregate result of individual peer connections closing [1][2]. Regarding the order of operations: - When a peer leaves (e.g., via room.leave() or a browser tab closing), the underlying WebRTC data channel closes [7][8]. - The onPeerLeave callback is triggered when the library detects that the connection to a specific peer has been lost [7][8]. - Because these are individual P2P connections, there is no guaranteed global order of disconnection across the room [1][2]. One peer may detect another's departure before others do [2]. If you send a message via send() just as a peer connection is closing, the Promise returned by send() may resolve successfully if the data was handed off to the browser's networking stack before the channel fully finalized its closure, but the data will not be received by the departing peer [3][5][9]. If the channel is already closed, attempts to send data will fail [9]. To handle this, rely on onPeerLeave to clean up state and prevent sending messages to disconnected peers [7][8].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Candidate files =="
git ls-files | rg '(^src/features/session/SessionView\.tsx$|lifecycle\.ts$|package\.json$|package-lock\.json$|pnpm-lock\.yaml$|yarn\.lock$)' || true

echo
echo "== SessionView context =="
if [ -f src/features/session/SessionView.tsx ]; then
  nl -ba src/features/session/SessionView.tsx | sed -n '680,750p'
fi

echo
echo "== lifecycle onPeerLeave references =="
for f in $(git ls-files | rg 'lifecycle\.ts$'); do
  echo "--- $f"
  nl -ba "$f" | sed -n '1,260p'
done

echo
echo "== package trystero version references =="
for f in $(git ls-files | rg '(^|/)package(-lock)?\.json$|pnpm-lock\.yaml$|yarn\.lock$'); do
  if rg -q 'trystero' "$f"; then
    echo "--- $f"
    rg -n '"?trystero"?|trystero' "$f" || true
  fi
done

echo
echo "== send calls around verify/incoming audit ="
rg -n "send\\(|verifyIncomingAuditEvent|markPeerDeparted|departedPeerIds|onPeerLeave" src || true

Repository: scotej/studyvis

Length of output: 345


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Candidate files =="
git ls-files | rg '(^src/features/session/SessionView\.tsx$|lifecycle\.ts$|package\.json$|package-lock\.json$|pnpm-lock\.yaml$|yarn\.lock$)' || true

echo
echo "== SessionView context =="
if [ -f src/features/session/SessionView.tsx ]; then
  nl -ba src/features/session/SessionView.tsx | sed -n '680,750p'
fi

echo
echo "== lifecycle onPeerLeave references =="
for f in $(git ls-files | rg 'lifecycle\.ts$'); do
  echo "--- $f"
  nl -ba "$f" | sed -n '1,260p'
done

echo
echo "== package trystero version references =="
for f in $(git ls-files | rg '(^|/)package(-lock)?\.json$|pnpm-lock\.yaml$|yarn\.lock$'); do
  if rg -q 'trystero' "$f"; then
    echo "--- $f"
    rg -n '"?trystero"?|trystero' "$f" || true
  fi
done

echo
echo "== send calls around verify/incoming audit =="
rg -n "send\\(|verifyIncomingAuditEvent|markPeerDeparted|departedPeerIds|onPeerLeave" src || true

Repository: scotej/studyvis

Length of output: 345


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== SessionView context =="
if [ -f src/features/session/SessionView.tsx ]; then
  awk '{printf "%6d\t%s\n", NR, $0}' src/features/session/SessionView.tsx | sed -n '690,740p'
fi

echo
echo "== lifecycle onPeerLeave references =="
for f in $(git ls-files | rg 'lifecycle\.ts$'); do
  echo "--- $f"
  awk '{printf "%6d\t%s\n", NR, $0}' "$f" | sed -n '1,260p'
done

echo
echo "== trystero version references =="
for f in package.json package-lock.json; do
  if [ -f "$f" ]; then
    echo "--- $f"
    grep -n 'trystero' "$f" || true
  fi
done

echo
echo "== focused send/incoming audit/lifecycle references =="
grep -RnE "send\\(|verifyIncomingAuditEvent|markPeerDeparted|departedPeerIds|onPeerLeave" src || true

Repository: scotej/studyvis

Length of output: 21775


🌐 Web query:

trystero 0.24.0 source get/send Promise resolves on receiveAck webRTC data channel

💡 Result:

In Trystero version 0.24.0, the send() method of a message action returns a Promise that resolves when the local sending process is complete, not when the receiver has acknowledged the message [1][2][3]. Trystero actions are built on top of WebRTC data channels [4][5]. The library abstracts communication into "Message actions" and "Request actions" [5][3]. 1. Message Actions: The send() method is designed for "fire-and-forget" events [5]. The returned Promise indicates that the data has been successfully sent from the local peer, but it does not wait for a formal application-level acknowledgment (ACK) from the receiver [1][2][3]. 2. Request Actions: If you require confirmation that a peer has received and processed a message, you should use a "Request action" [5][6]. By setting the action kind to 'request' (kind: 'request'), the request() method returns a Promise that resolves only when the target peer provides a response [5][6]. This mechanism naturally handles the acknowledgment and return of data [5][6]. In summary, the send() Promise in Trystero does not resolve on a receive-ACK; it resolves upon the completion of the local transmission [1][2][3]. For reliable receipt confirmation, use the request/response pattern instead [5][6].

Citations:


🌐 Web query:

trystero 0.24.0 source onPeerClose onPeerLeave data channel closed reason code 4000

💡 Result:

In Trystero version 0.24.0, the "4000" reason code appearing when a data channel closes is not a specific Trystero-defined error code, but rather a standard WebRTC RTCDataChannel close code [1][2]. WebRTC data channel close codes in the 4000 range are typically used by browsers or the underlying WebRTC implementation to indicate that the channel was closed due to an application-level event or a non-standard error condition [1][2]. In the context of Trystero, which handles peer-to-peer signaling and connection management, a "4000" code often arises when a peer connection is terminated, either gracefully via a call to room.leave or due to a connection disruption (such as network loss, a peer closing their tab, or a timeout) [3][4][1]. When Trystero triggers onPeerLeave, it indicates that a peer has been removed from your local room instance [3][5]. While Trystero manages the signaling and connection lifecycle, the underlying WebSocket or RTCDataChannel may report a close event with code 4000 to the browser, signifying the end of the transport stream [1]. If you are seeing this code frequently, it usually suggests the expected lifecycle of the P2P connection—either intentional (leaving the room) or unintentional (network instability)—is being captured by the browser's RTC implementation [4][1]. Trystero 0.24.0, released in April 2026, focused on internal improvements including a new WebSocket relay package and more proactive announce cycles, but it did not introduce custom Trystero-specific error codes for data channel closures [6][7]. If you are encountering these closures unexpectedly, it is often related to the stability of the signaling strategy being used (e.g., Nostr, MQTT, or BitTorrent) or NAT traversal issues [8][9].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== lifecycle.ts relevant handlers =="
for f in $(git ls-files | rg 'lifecycle\.ts$'); do
  echo "--- $f"
  awk '{printf "%6d\t%s\n", NR, $0}' "$f" | sed -n '400,440p'
done

echo
echo "== SessionView peerLeave/receive context =="
if [ -f src/features/session/SessionView.tsx ]; then
  awk '{printf "%6d\t%s\n", NR, $0}' src/features/session/SessionView.tsx | sed -n '470,540p'
  awk '{printf "%6d\t%s\n", NR, $0}' src/features/session/SessionView.tsx | sed -n '620,650p'
fi

echo
echo "== trystero wrapper context =="
if [ -f src/lib/trystero/index.ts ]; then
  awk '{printf "%6d\t%s\n", NR, $0}' src/lib/trystero/index.ts | sed -n '130,170p'
  awk '{printf "%6d\t%s\n", NR, $0}' src/lib/trystero/index.ts | sed -n '250,305p'
  awk '{printf "%6d\t%s\n", NR, $0}' src/lib/trystero/index.ts | sed -n '335,365p'
fi

echo
echo "== sessionStore departed peer helpers =="
if [ -f src/stores/sessionStore.ts ]; then
  awk '{printf "%6d\t%s\n", NR, $0}' src/stores/sessionStore.ts | sed -n '175,260p'
fi

Repository: scotej/studyvis

Length of output: 14342


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== SessionView handleLeave/buildAudit context =="
if [ -f src/features/session/SessionView.tsx ]; then
  awk '{printf "%6d\t%s\n", NR, $0}' src/features/session/SessionView.tsx | sed -n '540,580p'
  awk '{printf "%6d\t%s\n", NR, $0}' src/features/session/SessionView.tsx | sed -n '660,705p'
fi

echo
echo "== verifyIncomingAuditEvent shape =="
if [ -f src/stores/auditStore.ts ]; then
  awk '{printf "%6d\t%s\n", NR, $0}' src/stores/auditStore.ts | sed -n '140,195p'
fi

echo
echo "== lifecycle armGrace/end peer helper context =="
if [ -f src/features/session/lifecycle.ts ]; then
  awk '{printf "%6d\t%s\n", NR, $0}' src/features/session/lifecycle.ts | sed -n '258,325p'
fi

Repository: scotej/studyvis

Length of output: 8769


Don’t rely on auditAction.send() resolving before onPeerLeave.

Trystero’s message-action send() resolves after local buffering/local transmission, not after peer receipt, while onPeerLeave is driven from connection/data-channel teardown. A deliberate leave can arrive on onPeerLeave before the left audit message is delivered, so markPeerDeparted(peerId) would be skipped and the session can fall through the 20s auto-end grace instead of taking the no-grace ‘peer’ path. Use a request/response acknowledgment for the departure mark, or add the mark through the same teardown path that triggers the leave notification.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/session/SessionView.tsx` around lines 725 - 732, The departure
handling in SessionView’s verified left branch incorrectly assumes the
auditAction.send() completion guarantees peer receipt before onPeerLeave.
Replace this ordering dependency with a request/response acknowledgment, or
invoke markPeerDeparted(peerId) through the same teardown path that emits the
leave notification, ensuring deliberate departures always take the immediate
peer path.

// Long enough to actually scroll — the keyboard-reachability case.
export const Overflowing: Story = {
render: () => (
<div style={{ height: 480 }} className="flex bg-bg-base">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use a design token for the story viewport height.

Line 76 adds a hard-coded visual value in TSX. Use an existing layout token or shared Storybook fixture/decorator while preserving the overflow condition.

As per coding guidelines, frontend TSX should use design tokens instead of hard-coded visual values.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/stories/AuditLogPanel.stories.tsx` at line 76, Replace the hard-coded
480px height on the AuditLogPanel story’s wrapper div with the existing layout
design token or shared Storybook viewport fixture, while preserving the current
overflow-testing behavior and flex/background classes.

Source: Coding guidelines

Textually clean merge; overlapping files (lib.rs, index.css, strings.ts,
ARCHITECTURE.md, DESIGN-SYSTEM.md) touched disjoint hunks.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants