Skip to content

fix(reticulum): block RNCP sends to unreachable receive dests - #747

Merged
rinchen merged 4 commits into
mainfrom
fix/rncp-reachability-and-offer-ux
Jul 31, 2026
Merged

fix(reticulum): block RNCP sends to unreachable receive dests#747
rinchen merged 4 commits into
mainfrom
fix/rncp-reachability-and-offer-ux

Conversation

@rinchen

@rinchen rinchen commented Jul 31, 2026

Copy link
Copy Markdown
Member

Summary

  • Probe the rncp receive destination (not just LXMF) before Chat/Remote send, fetch, and retry; hard-block unreachable peers so transfers do not sit at 0%.
  • When the receive dest has no path but the LXMF peer is reachable, open a confirm modal to send an enable-request instead of starting a dead transfer.
  • Sticky Chat DM banner for pending ASK offers (Accept/Reject) and a Sidebar Remote badge from pendingOffers.
  • Condensed enable-request LXMF body asks the peer to reply with their rncp receive hash.

Test plan

  • With inbound listening Off, attempt RNCP send to a peer who is online in Chat → confirm modal → Send enable request works; no transfer starts at 0%.
  • Enable-request chat message is short and asks for their rncp receive hash.
  • With peer fully offline → error toast only; rncp.send not invoked.
  • With receive dest path-reachable → send/fetch proceed as before.
  • Remote retry hard-blocks without enable-request modal when unreachable.
  • Pending ASK offer for open DM shows sticky banner Accept/Reject above composer.
  • Sidebar Remote tab shows amber badge when pendingOffers > 0.

Summary by CodeRabbit

  • New Features
    • Added pending file-transfer indicators to Remote conversations and the sidebar.
    • Added Accept and Reject actions for incoming file-transfer offers.
    • Added destination availability checks before sending, fetching, or retrying transfers.
    • Added prompts to request listener enablement when a peer appears offline.
    • Improved transfer controls with busy states, status labels, and accessibility feedback.
  • Bug Fixes
    • Prevented transfers to unreachable or invalid destinations and provided clearer notifications.

rinchen added 2 commits July 31, 2026 08:17
Probe the rncp receive destination before send/fetch/retry so stuck 0% transfers are not queued when the peer is offline or inbound listening is off; prompt an enable request when chat still looks reachable, and surface pending ASK offers on Chat and the Remote sidebar badge.
Ask the peer to enable receive and reply with their rncp receive hash so senders can autofill a live destination.
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

RNCP transfers now perform destination reachability checks before transfer actions. Reticulum DM views display matching pending offers with accept or reject actions. The Remote sidebar tab displays pending-offer counts.

Changes

RNCP transfer flow

Layer / File(s) Summary
Reachability preflight
src/renderer/lib/ensureRncpDestinationReachable.ts, src/renderer/lib/ensureRncpDestinationReachable.test.ts
The new helper validates RNCP and LXMF hashes, probes destinations for reachability, and returns classifications: reachable with optional hop count, listenerLikelyOff when LXMF succeeds but RNCP fails, or peerUnreachable for invalid or inaccessible destinations. Tests cover probe arguments, fallback probing, and hash validation.
Chat DM RNCP control gating
src/renderer/components/remote/ChatDmRncpControl.tsx, src/renderer/components/remote/ChatDmRncpControl.test.tsx
The send flow now checks destination reachability before opening the file picker. Unreachable peers show an error toast. Likely-disabled listeners open a confirmation modal to request listener enablement. File selection proceeds only after successful reachability validation. The send button displays a reachability-checking label and aria-busy state during validation. Tests cover all three reachability outcomes.
Remote transfer section gating
src/renderer/components/remote/RemoteTransferSection.tsx, src/renderer/components/remote/RemoteTransferSection.test.tsx
Send, fetch, and retry operations now verify destination reachability before proceeding. Enable requests resolve and validate saved LXMF peer hashes against the RNCP destination. A confirmation modal prompts users to request listener enablement for disabled listeners. Busy states and reachability-checking labels disable controls during validation. Retry operations prevent concurrent attempts and check reachability without opening the enablement modal. Tests cover unreachable peers, listener-disabled confirmation, successful sends and fetches, and saved-hash validation.
Inbound offer presentation
src/renderer/App.tsx, src/renderer/components/ChatPanel.tsx, src/renderer/components/Sidebar.tsx, src/renderer/components/remote/ChatDmRncpOfferBanner.tsx, src/renderer/components/Sidebar.test.tsx, src/renderer/components/remote/ChatDmRncpOfferBanner.test.tsx
The App derives pending RNCP offer counts from the transfer store and passes them to the Sidebar only for Reticulum protocols with remote panel capability. The Sidebar displays pending-offer counts with amber badges and a dedicated accessibility label. ChatPanel renders the ChatDmRncpOfferBanner for eligible Reticulum DMs with RNCP transfer enabled and a resolved peer destination hash. The banner filters pending offers by LXMF peer identity, provides localized Accept and Reject actions via IPC, prevents duplicate or opposing clicks while processing, and removes offers after acceptance resolves. Tests cover offer filtering, IPC invocation, state cleanup, and concurrent-action safety.

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

Sequence Diagram(s)

sequenceDiagram
  participant ChatDmRncpControl
  participant ensureRncpDestinationReachable
  participant probeReticulumPeer
  participant ConfirmModal
  participant RNCPTransfer

  ChatDmRncpControl->>ensureRncpDestinationReachable: check destination
  ensureRncpDestinationReachable->>probeReticulumPeer: probe RNCP hash
  probeReticulumPeer-->>ensureRncpDestinationReachable: reachability result
  ensureRncpDestinationReachable-->>ChatDmRncpControl: return status
  ChatDmRncpControl->>ConfirmModal: confirm listener enablement when needed
  ConfirmModal-->>ChatDmRncpControl: confirm request
  ChatDmRncpControl->>RNCPTransfer: send file after successful check
Loading

Possibly related PRs

  • Colorado-Mesh/mesh-client#734: Both PRs directly modify RNCP transfer UI and behavior, including App.tsx, ChatPanel.tsx, ChatDmRncpControl.tsx, and related destination offer handling.
  • Colorado-Mesh/mesh-client#736: The main PR builds on RNCP transfer functionality from PR #736, using its listener state, offers, and reachability and enablement flows in the renderer.
  • Colorado-Mesh/mesh-client#741: Both PRs contribute to RNCP transfer functionality within the RemoteTransferSection component, with the main PR adding destination reachability validation and related UI components while the retrieved PR includes test coverage enhancements and RNCP transfer UI helper testing.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary change: blocking RNCP sends when the receive destination is unreachable.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
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
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/rncp-reachability-and-offer-ux

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: 2

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/renderer/components/remote/ChatDmRncpControl.tsx (1)

192-259: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Consider extracting the reachability-gate logic shared with RemoteTransferSection.tsx.

This block reimplements the same three-way branch (reachable / listenerLikelyOff / peerUnreachable) and confirm-modal wiring that RemoteTransferSection.tsx's assertReachableForTransfer (Lines 97-113) implements independently. Extracting a shared hook, for example useRncpReachabilityGate(lxmfPeerHash) returning { checkReachable, enableRequestConfirmOpen, closeEnableRequestConfirm }, would remove the duplication and reduce the risk of the two call sites diverging in behavior (see the related comment on RemoteTransferSection.tsx's handleRetry, where a related bug slipped into one implementation but not the other).

🤖 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/renderer/components/remote/ChatDmRncpControl.tsx` around lines 192 - 259,
Extract the shared RNCP reachability and listener-confirmation flow from
handleSend and RemoteTransferSection’s assertReachableForTransfer into a
reusable useRncpReachabilityGate hook. Preserve the reachable,
listenerLikelyOff, and peerUnreachable outcomes, including
enableRequestConfirmOpen state and its close handler, then update both call
sites to use the hook and remove their duplicated branching.
src/renderer/components/remote/RemoteTransferSection.tsx (1)

233-246: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Move incrementRetry(transferId) after the reachability check succeeds.

In handleRetry (lines 233–246), incrementRetry(transferId) at line 239 increments the stored transfer's retryCount before the reachability gate at lines 241–246. The incrementRetry function uses Zustand's set() to mutate the transfer record in the store immediately. If assertReachableForTransfer returns false, the function returns at line 245 without attempting a retry, but retryCount is already incremented. When the destination remains transiently unreachable across multiple manual or automatic retry attempts, the retry count reaches maxRetry (line 675 hides the Retry button when transfer.retryCount >= maxRetry) without any real retry attempt occurring. The user then loses the ability to retry even after the destination becomes reachable.

Move incrementRetry to execute only after assertReachableForTransfer succeeds, so the count reflects actual retry attempts.

Do you want me to generate a regression test for this path once the fix is in place?

🤖 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/renderer/components/remote/RemoteTransferSection.tsx` around lines 233 -
246, Move the incrementRetry(transferId) call in handleRetry to after
assertReachableForTransfer succeeds, while preserving the existing early return
when reachability fails. This ensures retryCount changes only when an actual
retry attempt proceeds.
🟡 Other comments (4)
src/renderer/components/Sidebar.test.tsx-241-256 (1)

241-256: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add axe coverage for the new RNCP badge and banner.

Both new UI elements use saturated colors and interactive controls. Add hydrateAxeThemeColors() and axe() assertions for their visible states.

  • src/renderer/components/Sidebar.test.tsx#L241-L256: Assert no axe violations when the Remote pending-offer badge is visible.
  • src/renderer/components/remote/ChatDmRncpOfferBanner.test.tsx#L35-L64: Assert no axe violations when a matching offer renders Accept and Reject controls.

As per coding guidelines, “assert accessibility with vitest-axe; call hydrateAxeThemeColors() instead of mocking themeColors.” As per path instructions, “For new badges/banners and saturated color combinations, add axe accessibility coverage and ensure contrast.”

🤖 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/renderer/components/Sidebar.test.tsx` around lines 241 - 256, Add
vitest-axe coverage for both new UI states: in
src/renderer/components/Sidebar.test.tsx lines 241-256, call
hydrateAxeThemeColors() and assert axe() reports no violations after rendering
the visible Remote pending-offer badge; in
src/renderer/components/remote/ChatDmRncpOfferBanner.test.tsx lines 35-64, do
the same for a matching RNCP offer rendering Accept and Reject controls. Do not
mock themeColors.

Sources: Coding guidelines, Path instructions

src/renderer/components/Sidebar.tsx-81-89 (1)

81-89: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Match the capped accessible count to the visible badge.

When remotePendingOffers exceeds 99, the badge displays 99+ but the accessible label receives 99. Pass '99+' to the translation in this case, as aria.tabWithUnread already does.

Proposed fix
-                  count: badgeCount > 99 ? 99 : badgeCount,
+                  count: badgeCount > 99 ? '99+' : badgeCount,
🤖 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/renderer/components/Sidebar.tsx` around lines 81 - 89, Update the
showRemoteBadge branch of the tabAriaLabel calculation to pass the capped string
'99+' when badgeCount exceeds 99, matching the visible badge; retain the numeric
badgeCount for values up to 99 and leave the aria.tabWithUnread branch
unchanged.
src/renderer/components/Sidebar.tsx-121-124 (1)

121-124: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a higher-contrast Remote badge color.

bg-amber-600 with text-white fails contrast requirements for the 10px badge text. Use a darker amber utility, such as bg-amber-800, and cover this state with vitest-axe.

Proposed fix
-                      showRemoteBadge ? 'bg-amber-600' : 'bg-red-600'
+                      showRemoteBadge ? 'bg-amber-800' : 'bg-red-600'

As per path instructions, “For new badges/banners and saturated color combinations, add axe accessibility coverage and ensure contrast.”

🤖 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/renderer/components/Sidebar.tsx` around lines 121 - 124, Update the
remote state styling in the Sidebar badge className to use a darker amber
utility such as bg-amber-800 with white text, preserving the existing red
styling for non-remote badges. Add vitest-axe coverage that exercises the remote
badge state and verifies it has no accessibility violations.

Source: Path instructions

src/renderer/components/remote/RemoteTransferSection.tsx-97-113 (1)

97-113: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Generic error message during retry misrepresents listenerLikelyOff.

When opts.promptEnable is false (the handleRetry path) and reach.status === 'listenerLikelyOff', the code falls through to the same toast used for peerUnreachable: "the peer may be offline." In this state the LXMF peer is actually reachable; only the receive listener appears disabled. Showing the generic offline message during automatic or manual retries can mislead the user into believing the destination is completely gone, when sending an enable request would resolve it.

Use a distinct message for this branch, for example a toast that mentions the listener is likely off, so retry failures are diagnosable.

🤖 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/renderer/components/remote/RemoteTransferSection.tsx` around lines 97 -
113, Update assertReachableForTransfer so listenerLikelyOff remains a distinct
branch when opts.promptEnable is false, displaying a listener-specific error
toast instead of the generic peerUnreachable message; preserve the existing
confirmation flow when prompting is enabled and the generic toast for other
unreachable statuses.
🤖 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 `@src/renderer/components/remote/ChatDmRncpOfferBanner.tsx`:
- Around line 71-85: Update the pending-offer action flow around handleAccept
and handleReject to track in-flight state per transfer_id, synchronously
blocking repeated or opposite actions before React renders disabled controls.
Disable both Accept and Reject buttons for that offer while either helper is
pending, and clear the per-transfer state in finally so failed accepts remain
retryable. Add a behavioral test that keeps Accept pending, clicks Reject, and
verifies only one IPC request is issued.

In `@src/renderer/lib/ensureRncpDestinationReachable.ts`:
- Around line 27-30: Extract the repeated 32-character hexadecimal validation
into a shared isRncpHexHash(value: string) helper, then replace the inline
checks in ensureRncpDestinationReachable and RemoteTransferSection’s
resolveLxmfPeerHash with that helper. Preserve the existing trimming and
lowercasing behavior before validation.

---

Outside diff comments:
In `@src/renderer/components/remote/ChatDmRncpControl.tsx`:
- Around line 192-259: Extract the shared RNCP reachability and
listener-confirmation flow from handleSend and RemoteTransferSection’s
assertReachableForTransfer into a reusable useRncpReachabilityGate hook.
Preserve the reachable, listenerLikelyOff, and peerUnreachable outcomes,
including enableRequestConfirmOpen state and its close handler, then update both
call sites to use the hook and remove their duplicated branching.

In `@src/renderer/components/remote/RemoteTransferSection.tsx`:
- Around line 233-246: Move the incrementRetry(transferId) call in handleRetry
to after assertReachableForTransfer succeeds, while preserving the existing
early return when reachability fails. This ensures retryCount changes only when
an actual retry attempt proceeds.

---

Other comments:
In `@src/renderer/components/remote/RemoteTransferSection.tsx`:
- Around line 97-113: Update assertReachableForTransfer so listenerLikelyOff
remains a distinct branch when opts.promptEnable is false, displaying a
listener-specific error toast instead of the generic peerUnreachable message;
preserve the existing confirmation flow when prompting is enabled and the
generic toast for other unreachable statuses.

In `@src/renderer/components/Sidebar.test.tsx`:
- Around line 241-256: Add vitest-axe coverage for both new UI states: in
src/renderer/components/Sidebar.test.tsx lines 241-256, call
hydrateAxeThemeColors() and assert axe() reports no violations after rendering
the visible Remote pending-offer badge; in
src/renderer/components/remote/ChatDmRncpOfferBanner.test.tsx lines 35-64, do
the same for a matching RNCP offer rendering Accept and Reject controls. Do not
mock themeColors.

In `@src/renderer/components/Sidebar.tsx`:
- Around line 81-89: Update the showRemoteBadge branch of the tabAriaLabel
calculation to pass the capped string '99+' when badgeCount exceeds 99, matching
the visible badge; retain the numeric badgeCount for values up to 99 and leave
the aria.tabWithUnread branch unchanged.
- Around line 121-124: Update the remote state styling in the Sidebar badge
className to use a darker amber utility such as bg-amber-800 with white text,
preserving the existing red styling for non-remote badges. Add vitest-axe
coverage that exercises the remote badge state and verifies it has no
accessibility violations.
🪄 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: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 2797378e-4c8f-4731-a3a0-304a0d3e7faf

📥 Commits

Reviewing files that changed from the base of the PR and between 70ec7bb and ae60bff.

⛔ Files ignored due to path filters (16)
  • src/renderer/locales/cs/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/de/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/en/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/es/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/fr/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/id/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/it/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/ja/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/ko/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/nl/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/pl/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/pt-BR/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/ru/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/tr/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/uk/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/zh/translation.json is excluded by !src/renderer/locales/**
📒 Files selected for processing (12)
  • src/renderer/App.tsx
  • src/renderer/components/ChatPanel.tsx
  • src/renderer/components/Sidebar.test.tsx
  • src/renderer/components/Sidebar.tsx
  • src/renderer/components/remote/ChatDmRncpControl.test.tsx
  • src/renderer/components/remote/ChatDmRncpControl.tsx
  • src/renderer/components/remote/ChatDmRncpOfferBanner.test.tsx
  • src/renderer/components/remote/ChatDmRncpOfferBanner.tsx
  • src/renderer/components/remote/RemoteTransferSection.test.tsx
  • src/renderer/components/remote/RemoteTransferSection.tsx
  • src/renderer/lib/ensureRncpDestinationReachable.test.ts
  • src/renderer/lib/ensureRncpDestinationReachable.ts

Comment thread src/renderer/components/remote/ChatDmRncpOfferBanner.tsx Outdated
Comment on lines +27 to +30
const destinationHash = args.destinationHash.trim().toLowerCase();
if (!/^[0-9a-f]{32}$/.test(destinationHash)) {
return { status: 'peerUnreachable' };
}

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 | 🟠 Major | ⚡ Quick win

Extract the hex-hash validator to reduce duplication.

The /^[0-9a-f]{32}$/ check appears twice in this file (Line 28 and Line 44), and an equivalent check is duplicated in RemoteTransferSection.tsx's resolveLxmfPeerHash (Line 94). Extract a small shared helper, for example isRncpHexHash(value: string): boolean, and reuse it in both files. This keeps the destination-hash and LXMF-hash validation rules consistent if the format ever changes.

♻️ Proposed helper extraction
+export function isRncpHexHash(value: string): boolean {
+  return /^[0-9a-f]{32}$/.test(value);
+}
+
 export async function ensureRncpDestinationReachable(
   args: EnsureRncpDestinationReachableArgs,
 ): Promise<RncpDestinationReachability> {
   const destinationHash = args.destinationHash.trim().toLowerCase();
-  if (!/^[0-9a-f]{32}$/.test(destinationHash)) {
+  if (!isRncpHexHash(destinationHash)) {
     return { status: 'peerUnreachable' };
   }
   ...
   const lxmf = args.lxmfPeerHash?.trim().toLowerCase() ?? '';
-  if (!/^[0-9a-f]{32}$/.test(lxmf) || lxmf === destinationHash) {
+  if (!isRncpHexHash(lxmf) || lxmf === destinationHash) {
     return { status: 'peerUnreachable' };
   }

Also applies to: 43-46

🤖 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/renderer/lib/ensureRncpDestinationReachable.ts` around lines 27 - 30,
Extract the repeated 32-character hexadecimal validation into a shared
isRncpHexHash(value: string) helper, then replace the inline checks in
ensureRncpDestinationReachable and RemoteTransferSection’s resolveLxmfPeerHash
with that helper. Preserve the existing trimming and lowercasing behavior before
validation.

Block double Accept/Reject on the Chat DM offer banner with per-transfer in-flight state, and reuse isRncpHexHash for preflight/LXMF peer validation.

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/renderer/components/remote/RemoteTransferSection.tsx (2)

379-379: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Require a validated, non-duplicate LXMF target.

resolveLxmfPeerHash returns null when the saved LXMF value is missing or invalid, but ?? parsedHash then passes the generic destination field to sendRncpRequestEnable. This field can contain an RNCP receive hash, not an LXMF delivery hash. The direct request path also does not reject a saved LXMF hash equal to parsedHash, although the preflight helper rejects that duplicate.

Require a validated, non-duplicate LXMF target. If direct LXMF input is supported, preserve the input kind and validate it separately before sending.

As per path instructions, AGENTS.md requires “validate destination and LXMF hashes at boundaries” and reuse existing RNCP validation helpers.

🤖 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/renderer/components/remote/RemoteTransferSection.tsx` at line 379, Update
the direct transfer path around peerLxmfHash so sendRncpRequestEnable receives
only a validated LXMF delivery hash; remove the fallback to parsedHash, reject
missing or invalid resolved values, and reject a saved LXMF hash equal to
parsedHash using the existing RNCP validation helpers. If direct LXMF input is
supported, preserve its input type and validate it separately at this boundary.

Source: Path instructions


244-249: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Preserve retry state and prevent duplicate retry submissions.

incrementRetry(transferId) runs before assertReachableForTransfer at Line 242. If reachability fails, no RNCP request starts, but the transfer still consumes a retry. The automatic retry marker is also set before handleRetry runs at Lines 296-299, so a blocked probe can prevent recovery after the peer becomes reachable.

Add a per-transfer in-flight guard before the probe. Run the probe before changing retry state. Increment the retry count only when the sidecar accepts the resubmission.

As per path instructions, AGENTS.md requires “preserve state on probe/send failures” and “guard duplicate actions while async transfers or offer actions are in flight.”

🤖 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/renderer/components/remote/RemoteTransferSection.tsx` around lines 244 -
249, Update the retry flow around handleRetry and the automatic retry marker to
guard each transfer against concurrent retry submissions before
assertReachableForTransfer. Run the reachability probe first, leaving retry
state and automatic retry markers unchanged when it fails; incrementRetry and
mark the retry as in flight only after the sidecar accepts the resubmission, and
clear the guard when the submission completes or fails.

Source: Path instructions

🧹 Nitpick comments (1)
src/renderer/components/remote/RemoteTransferSection.test.tsx (1)

161-206: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the new fetch, retry, and enable-request boundary branches.

The changed tests cover send success, unreachable peers, and listener-likely-off confirmation. The component changes also gate fetch and retry operations. Add or verify tests for blocked fetches, blocked retries, and missing, invalid, or duplicate LXMF targets. Assert that blocked operations do not invoke RNCP IPC or dispatch an enable request.

As per path instructions, AGENTS.md requires Vitest coverage for “every behavioral path,” including unreachable peers, listener-disabled fallback, and invalid/duplicate hashes.

#!/bin/bash
set -euo pipefail
rg -n -C 4 'fetch|retry|sendRncpRequestEnable|lxmf_peer_hash|invalid|duplicate' \
  src/renderer/components/remote/RemoteTransferSection.test.tsx
🤖 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/renderer/components/remote/RemoteTransferSection.test.tsx` around lines
161 - 206, Expand the RemoteTransferSection tests to cover fetch and retry
operations when the destination is unreachable, plus missing, invalid, or
duplicate LXMF target hashes. Verify listener-disabled fallback behavior and
assert each blocked operation neither invokes RNCP IPC nor dispatches
sendRncpRequestEnable; retain the existing send success and confirmation
coverage.
🤖 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.

Outside diff comments:
In `@src/renderer/components/remote/RemoteTransferSection.tsx`:
- Line 379: Update the direct transfer path around peerLxmfHash so
sendRncpRequestEnable receives only a validated LXMF delivery hash; remove the
fallback to parsedHash, reject missing or invalid resolved values, and reject a
saved LXMF hash equal to parsedHash using the existing RNCP validation helpers.
If direct LXMF input is supported, preserve its input type and validate it
separately at this boundary.
- Around line 244-249: Update the retry flow around handleRetry and the
automatic retry marker to guard each transfer against concurrent retry
submissions before assertReachableForTransfer. Run the reachability probe first,
leaving retry state and automatic retry markers unchanged when it fails;
incrementRetry and mark the retry as in flight only after the sidecar accepts
the resubmission, and clear the guard when the submission completes or fails.

---

Nitpick comments:
In `@src/renderer/components/remote/RemoteTransferSection.test.tsx`:
- Around line 161-206: Expand the RemoteTransferSection tests to cover fetch and
retry operations when the destination is unreachable, plus missing, invalid, or
duplicate LXMF target hashes. Verify listener-disabled fallback behavior and
assert each blocked operation neither invokes RNCP IPC nor dispatches
sendRncpRequestEnable; retain the existing send success and confirmation
coverage.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 23b1fe08-5b2b-4c60-8e0e-62e85fac28ad

📥 Commits

Reviewing files that changed from the base of the PR and between ae60bff and fdf44a9.

📒 Files selected for processing (5)
  • src/renderer/components/remote/ChatDmRncpOfferBanner.test.tsx
  • src/renderer/components/remote/ChatDmRncpOfferBanner.tsx
  • src/renderer/components/remote/RemoteTransferSection.test.tsx
  • src/renderer/components/remote/RemoteTransferSection.tsx
  • src/renderer/lib/ensureRncpDestinationReachable.ts

Require a validated LXMF delivery hash for enable requests, and only bump retry state after the sidecar accepts a resubmit that passed the reachability probe.
@rinchen

rinchen commented Jul 31, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai resume

@coderabbitai review

Please re-review including commits after the pause (offer-action guard, hash helper, enable-request/retry gates).

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

@rinchen Resumed automated reviews. I will re-review pull request #747, including the offer-action guard, hash helper, enable-request gates, and retry gates.

✅ Action performed

Reviews resumed.


Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@rinchen
rinchen merged commit 25eae77 into main Jul 31, 2026
12 checks passed
@rinchen
rinchen deleted the fix/rncp-reachability-and-offer-ux branch July 31, 2026 15:03

@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.

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/renderer/components/remote/RemoteTransferSection.tsx (1)

101-117: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fix the toast message and silence it during unattended auto-retry.

assertReachableForTransfer shows the same peerUnreachable toast for both peerUnreachable and listenerLikelyOff whenever opts.promptEnable is false. During retry, this tells the user "the peer may be offline" even when the LXMF probe succeeded and only the RNCP listener is off. Use a status-specific message so retry failures accurately reflect the cause.

Separately, handleRetry is invoked automatically by the auto-retry effect (Lines 301-311) on mount whenever a failed transfer with retryArgs exists and settings.autoRetryTransfer is true (the default, per DEFAULT_REMOTE_SETTINGS). Because assertReachableForTransfer always calls addToast on non-reachable results, simply rendering this component with a pre-existing failed transfer triggers a background network probe and an error toast without any user action. Add a silent option so automated retries can suppress the toast, and let only user-initiated retries and sends surface it.

🐛 Proposed fix
   const assertReachableForTransfer = useCallback(
-    async (destinationHash: string, opts: { promptEnable: boolean }): Promise<boolean> => {
+    async (
+      destinationHash: string,
+      opts: { promptEnable: boolean; silent?: boolean },
+    ): Promise<boolean> => {
       const reach = await ensureRncpDestinationReachable({
         destinationHash,
         lxmfPeerHash: resolveLxmfPeerHash(destinationHash),
       });
       if (reach.status === 'reachable') return true;
       if (reach.status === 'listenerLikelyOff' && opts.promptEnable) {
         setEnableRequestConfirmOpen(true);
         return false;
       }
-      addToast(t('reticulumRemote.transfer.peerUnreachable'), 'error');
+      if (!opts.silent) {
+        addToast(
+          reach.status === 'listenerLikelyOff'
+            ? t('reticulumRemote.transfer.listenerLikelyOffBody')
+            : t('reticulumRemote.transfer.peerUnreachable'),
+          'error',
+        );
+      }
       return false;
     },
     [addToast, resolveLxmfPeerHash, t],
   );

Then pass silent: true from the automatic-retry call path (e.g., a parameter on handleRetry distinguishing manual clicks from the auto-retry effect) while keeping manual retry and send/fetch calls non-silent.

🤖 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/renderer/components/remote/RemoteTransferSection.tsx` around lines 101 -
117, Update assertReachableForTransfer to choose a status-specific unreachable
message, distinguishing listenerLikelyOff from peerUnreachable, and add a silent
option that suppresses toast emission. Thread this option through handleRetry
and pass silent: true from the automatic-retry effect, while keeping manual
retry and send/fetch paths non-silent.
🟡 Other comments (1)
src/renderer/components/remote/RemoteTransferSection.tsx-383-407 (1)

383-407: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a distinct error message for saved LXMF peer hash validation failures.

When savedForDest exists but its lxmf_peer_hash is missing, malformed, or equal to parsedHash, the code shows t('reticulumRemote.errors.invalidAddress'). Per the test assertions, this renders as "Enter a valid 32-character hex destination hash." That message tells the user to fix the destination-hash input field, but the actual problem is the saved LXMF peer hash in the address book, which this field does not edit. A user who re-types a correctly formatted destination hash still cannot resolve this error. Show a message that identifies the saved-peer-hash problem instead.

♻️ Proposed fix
     if (savedForDest) {
       const peer = savedForDest.lxmf_peer_hash?.trim().toLowerCase() ?? '';
       if (!isRncpHexHash(peer) || peer === parsedHash) {
-        addToast(t('reticulumRemote.errors.invalidAddress'), 'error');
+        addToast(t('reticulumRemote.errors.invalidSavedPeerHash'), 'error');
         return;
       }
       peerLxmfHash = peer;
     }
🤖 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/renderer/components/remote/RemoteTransferSection.tsx` around lines 383 -
407, Update the validation branch inside handleRequestEnable for an existing
savedForDest so missing, malformed, or destination-duplicate lxmf_peer_hash
values use a distinct saved-peer-hash error translation instead of
invalidAddress. Leave the destination-field validation and direct-hash fallback
behavior unchanged.
🤖 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.

Outside diff comments:
In `@src/renderer/components/remote/RemoteTransferSection.tsx`:
- Around line 101-117: Update assertReachableForTransfer to choose a
status-specific unreachable message, distinguishing listenerLikelyOff from
peerUnreachable, and add a silent option that suppresses toast emission. Thread
this option through handleRetry and pass silent: true from the automatic-retry
effect, while keeping manual retry and send/fetch paths non-silent.

---

Other comments:
In `@src/renderer/components/remote/RemoteTransferSection.tsx`:
- Around line 383-407: Update the validation branch inside handleRequestEnable
for an existing savedForDest so missing, malformed, or destination-duplicate
lxmf_peer_hash values use a distinct saved-peer-hash error translation instead
of invalidAddress. Leave the destination-field validation and direct-hash
fallback behavior unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 9bfe9c14-b131-428a-8f1c-574572d1c73d

📥 Commits

Reviewing files that changed from the base of the PR and between ae60bff and 4015a5a.

📒 Files selected for processing (5)
  • src/renderer/components/remote/ChatDmRncpOfferBanner.test.tsx
  • src/renderer/components/remote/ChatDmRncpOfferBanner.tsx
  • src/renderer/components/remote/RemoteTransferSection.test.tsx
  • src/renderer/components/remote/RemoteTransferSection.tsx
  • src/renderer/lib/ensureRncpDestinationReachable.ts

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.

1 participant