Skip to content

feat(reticulum): LXMF paper messages, voice connect tones, and self-hash allowlist - #793

Merged
rinchen merged 5 commits into
mainfrom
paper
Aug 5, 2026
Merged

feat(reticulum): LXMF paper messages, voice connect tones, and self-hash allowlist#793
rinchen merged 5 commits into
mainfrom
paper

Conversation

@rinchen

@rinchen rinchen commented Aug 5, 2026

Copy link
Copy Markdown
Member

Summary

This branch adds encrypted LXMF paper message create/ingest (QR / lxm:// offline handoff), hardens paper persist/IPC/i18n, improves LXST voice connect progress tones (modem handshake → carrier → UK ringback), and fixes the reticulumLastSelfLxmfHash appSettings:set allowlist so SQLite mirrors the last self LXMF hash again.

1. Encrypted LXMF paper messages (create + ingest)

Offline, identity-encrypted LXMF can be exchanged without RF via QR or lxm:// URI.

Sidecar

  • POST /api/v1/lxmf/paper/create — pack text for a destination hash into an encrypted paper URI
  • POST /api/v1/lxmf/paper/ingest — decrypt a paper URI into the local LXMF inbox
  • Stable API error codes (invalid_hash, paper_too_large, identity_not_configured, identity_unknown, invalid_uri, decrypt_failed, internal_error) normalized in map_paper_create_error / map_paper_ingest_error
  • Paper Completes immediately with delivery_method: "paper" (no lxmf_outbound_status path)

Renderer / shared

  • Chat DM Share as paper + Scan paper (ChatDmPaperControls) — draft → QR/URI; scan/paste via QrIngestControl
  • Network Scan / import and shared handleReticulumQrIngest for paper + contact/identity QR
  • Deep-link parse: lxmPaperMessage kind + looksLikeLxmPaperBlob (meshClientDeepLink.ts)
  • OS lxm:// paper deep links ingest without confirm (contacts / MeshCore still confirm)
  • Status badge Paper; received_via / delivery_method include paper
  • Shared reticulumMessageTransport + reticulumPaperErrors keep IPC allowlists and i18n error maps aligned

2. Paper harden (persist, helpers, i18n)

Follow-up fix so paper survives round-trips and errors stay actionable:

  • Allow paper through SQLite IPC (reticulum-db-handlers) so received_via: paper is not stripped on save (badge survives restart)
  • Route create/ingest through shared LXMF helpers; map sidecar codes → stable i18n keys
  • showReticulumQrIngestToast for consistent success/error toasts
  • check-i18n-quality.mjs: detect locale scheme breakage (lxm:// / related) that plain key checks missed
  • Docs: reticulum.md, reticulum-sidecar-ipc.md, troubleshooting table for paper create/ingest failures
  • Tests: ChatDmPaperControls, createReticulumPaperMessage, transport/errors, DB handlers, deep-link apply

3. LXST voice connect tones

Outbound call progress audio:

  • After peer DTMF fold, play a short modem handshake, then a quiet carrier bed
  • Cut to UK double-ring as soon as the call reaches connecting (replaces post-DTMF auto-ringback)
  • Covered in reticulumVoiceCallTones / session tests

4. reticulumLastSelfLxmfHash allowlist

Since #785, SQLite appSettings:set rejected the last LXMF self-hash key (localStorage still worked). Allowlist the key and lock it in contract/DB tests so the mirror persists across restarts.

Commits

Commit Description
61e3ae04 feat(reticulum): modem handshake then carrier during voice connect
82143ae2 fix(reticulum): allowlist reticulumLastSelfLxmfHash for appSettings:set
a80b0094 feat(reticulum): add encrypted LXMF paper message create and ingest
e044f2eb fix(reticulum): harden LXMF paper persist, ingest, and i18n

Test plan

  • Paper create (Chat): Reticulum stack running + peer known → open DM → type short draft → Share as paper → QR appears → Copy URI succeeds
  • Paper create errors: unknown peer → identity_unknown toast; oversized text → paper_too_large; stack stopped / no identity → appropriate failure
  • Paper ingest (Chat): Scan paper paste/QR decrypts into Chat with Paper badge; wrong identity slot → decrypt_failed
  • Paper ingest (Network): Scan/import paper URI/QR same as Chat
  • OS deep link: open lxm:// paper URI → ingests without confirm dialog; contact/lxma:// still confirm
  • Persist: restart app after paper create/ingest → Paper badge / received_via still present in Chat
  • Voice: place LXST outbound call → hear modem handshake → carrier → UK ringback when connecting
  • Self hash: after outbound LXMF, quit/relaunch → reticulumLastSelfLxmfHash still present in SQLite app_settings (not only localStorage)
  • pnpm run check:i18n / locale quality passes (scheme breakage rules)
  • Sidecar: pnpm run check:reticulum-sidecar (or Clippy + paper API unit tests) when cargo available

Summary by CodeRabbit

  • New Features
    • Added offline encrypted Reticulum paper messaging through QR codes and lxm:// links.
    • Added “Share as paper” and “Scan paper” controls in chats and Reticulum tools.
    • Added paper delivery labels and status indicators.
  • Bug Fixes
    • Improved QR/deep-link validation, error handling, and message persistence.
    • Improved outgoing Reticulum call tones with modem handshake and ringback transitions.
  • Documentation
    • Added setup, API, delivery, and troubleshooting guidance for paper messaging.

rinchen added 4 commits August 4, 2026 19:48
Replace post-DTMF auto-ringback with a short modem handshake and quiet
carrier bed; cut to UK ringback as soon as the call reaches connecting.
SQLite mirror of the last LXMF self hash was rejected by the IPC allowlist
since #785; localStorage still worked. Add the key and lock it in contracts.
Wire sidecar paper APIs, deep-link/QR ingest, Chat share/scan UI, and Network Scan/import so encrypted paper messages can be exchanged without RF.
Allow paper received_via through SQLite IPC, route create/ingest through shared LXMF helpers, map sidecar errors to stable codes, and fix locale scheme breakage check:i18n missed.
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 36 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

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: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 5084f19b-a2dc-409f-90a7-a98dcd83b02b

📥 Commits

Reviewing files that changed from the base of the PR and between e044f2e and 10f1e0d.

📒 Files selected for processing (15)
  • reticulum-sidecar/src/stack/live.rs
  • scripts/check-i18n-quality.mjs
  • scripts/check-i18n-quality.test.mjs
  • src/renderer/App.tsx
  • src/renderer/components/ChatDmPaperControls.test.tsx
  • src/renderer/components/ChatDmPaperControls.tsx
  • src/renderer/components/ChatPanel.tsx
  • src/renderer/components/ReticulumMessageStatusBadge.test.tsx
  • src/renderer/components/ReticulumMessageStatusBadge.tsx
  • src/renderer/lib/radio/BaseRadioProvider.ts
  • src/renderer/lib/radio/protocol-capabilities.test.ts
  • src/renderer/lib/reticulum/createReticulumPaperMessage.test.ts
  • src/renderer/lib/reticulumVoiceCallTones.test.ts
  • src/renderer/lib/reticulumVoiceCallTones.ts
  • src/shared/meshClientDeepLink.ts
📝 Walkthrough

Walkthrough

The PR adds encrypted LXMF paper-message sharing and scanning across the sidecar, renderer, persistence layer, UI, and documentation. It also adds shared transport validation and changes outgoing voice-call tones to use a modem handshake and carrier sequence before ringback.

Changes

Reticulum paper transport

Layer / File(s) Summary
Paper transport contracts and classification
src/shared/reticulumMessageTransport.ts, src/shared/reticulumPaperErrors.ts, src/shared/meshClientDeepLink.ts, src/renderer/lib/reticulum/...
Adds paper transport values, validation, error mappings, and lxm:// paper-message classification.
Paper creation and ingestion sidecar flow
reticulum-sidecar/src/api/..., reticulum-sidecar/src/stack/...
Adds paper URI creation and ingestion APIs, structured errors, delivery metadata, and WebSocket message events.
Renderer paper creation and QR ingestion
src/renderer/lib/reticulum/..., src/renderer/lib/meshClientDeepLinkApply.ts, src/renderer/hooks/..., src/renderer/components/ReticulumNetworkPanel.tsx
Adds paper creation, QR/deep-link ingestion, localized outcomes, and shared scan handling.
Paper chat UI and message persistence
src/renderer/components/..., src/main/..., src/renderer/lib/ingest/..., docs/..., scripts/check-i18n-quality.mjs
Adds chat sharing and scanning controls, Paper status labels, persisted transport values, translation checks, and documentation.

Outgoing voice-call tones

Layer / File(s) Summary
Modem handshake and ringback transition
src/renderer/lib/reticulumVoiceCallTones.ts, src/renderer/lib/reticulumVoiceSession.ts, src/renderer/runtime/...
Adds modem handshake audio, carrier playback, cleanup, explicit ringback promotion, and related tests.

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

Sequence Diagram(s)

sequenceDiagram
  participant ChatPanel
  participant ReticulumAPI
  participant LiveBridge
  participant ReticulumIdentity
  ChatPanel->>ReticulumAPI: Create encrypted paper message
  ReticulumAPI->>LiveBridge: Call create_lxmf_paper
  LiveBridge->>ReticulumIdentity: Resolve key and encrypt LXMF
  LiveBridge-->>ReticulumAPI: Return lxm:// URI
  ReticulumAPI-->>ChatPanel: Display QR and message status
  ChatPanel->>ReticulumAPI: Ingest scanned lxm:// URI
  ReticulumAPI->>LiveBridge: Call ingest_lxmf_paper
  LiveBridge-->>ReticulumAPI: Return decrypted LXMF payload
  ReticulumAPI-->>ChatPanel: Show ingestion result and persist message
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.07% 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 and concisely names the three primary changes: LXMF paper messages, voice connection tones, and self-hash persistence.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch paper

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

Note

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

🟡 Other comments (5)
src/renderer/lib/reticulumVoiceCallTones.ts-271-290 (1)

271-290: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Release finished modem nodes instead of accumulating them for the whole connecting phase.

trackModemStoppable and trackModemDisconnectable only append. Nothing removes an entry when a one-shot node finishes. The handshake adds five entries. Each heartbeat chirp adds two more every MODEM_HEARTBEAT_MS (2800 ms). An unanswered outgoing call stays in the connect sequence until the peer answers or the call fails, so the arrays grow for the whole duration and hold references to AudioNode objects that already finished. The later teardown loop then calls stop() on every finished node and relies on the catch blocks.

Untrack each one-shot node when it ends. The change is local to the chirp helper.

♻️ Proposed fix: untrack one-shot chirp nodes on ended
 function trackModemStoppable(node: {
   stop: (when?: number) => void;
   disconnect: () => void;
 }): void {
   modemStoppables.push(node);
 }
 
 function trackModemDisconnectable(node: { disconnect: () => void }): void {
   modemDisconnectables.push(node);
 }
+
+function untrackModemNode(
+  node: { stop?: (when?: number) => void; disconnect: () => void },
+): void {
+  modemStoppables = modemStoppables.filter((tracked) => tracked !== node);
+  modemDisconnectables = modemDisconnectables.filter((tracked) => tracked !== node);
+}
   osc.start(startTime);
   osc.stop(startTime + durationS);
   trackModemStoppable(osc);
   trackModemDisconnectable(gain);
+  osc.onended = () => {
+    try {
+      gain.disconnect();
+      osc.disconnect();
+    } catch {
+      // catch-no-log-ok already disconnected
+    }
+    untrackModemNode(osc);
+    untrackModemNode(gain);
+  };
 }

The chirp helper currently types its nodes through ctx.createOscillator() and ctx.createGain(), so onended is available. Keep the mock in reticulumVoiceCallTones.test.ts in step if you adopt this.

Also applies to: 379-384

🤖 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/reticulumVoiceCallTones.ts` around lines 271 - 290, Update
scheduleModemChirp so each one-shot oscillator and gain node is removed from the
modem tracking collections when the oscillator emits its ended event, while
preserving the existing tracking and teardown behavior. Add the corresponding
untracking support using the existing trackModemStoppable and
trackModemDisconnectable mechanisms, and update the test mock in
reticulumVoiceCallTones.test.ts if needed to expose ended handling.
src/renderer/components/ReticulumMessageStatusBadge.tsx-110-118 (1)

110-118: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Restrict the tooltip suffix omission to completed paper messages.

Line 118 drops statusLabel for every paper row, including status="failed". statusLabelText returns error ?? t('chatPanel.reticulumSendFailed') for a failure, so a failed paper row would show only the paper tooltip and hide the reason. The stated intent is to omit the suffix for paper completions.

🐛 Proposed fix
-  const tooltip = deliveryMethod === 'paper' ? viaPrefix : `${viaPrefix}: ${statusLabel}`;
+  const tooltip =
+    deliveryMethod === 'paper' && status === 'acked' ? viaPrefix : `${viaPrefix}: ${statusLabel}`;
🤖 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/ReticulumMessageStatusBadge.tsx` around lines 110 -
118, Update the tooltip construction near statusLabelText and viaPrefixText so
the status suffix is omitted only when deliveryMethod is paper and status
represents completion. Failed paper messages must retain `${viaPrefix}:
${statusLabel}`, including the error or failure text from statusLabelText;
preserve the existing paper-only prefix behavior for completed messages.
src/renderer/components/ChatDmPaperControls.tsx-103-123 (1)

103-123: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Move focus into the dialog on open and restore it on close.

The dialog renders with role="dialog" and aria-modal="true", but focus stays on the share button behind the overlay. A keyboard user must tab through the whole page to reach the textarea, and focus is not returned after close. Escape works only because the listener is attached to document.

♿ Proposed minimal fix
+  const dialogRef = useRef<HTMLDivElement | null>(null);
+  const triggerRef = useRef<HTMLButtonElement | null>(null);
+
+  useEffect(() => {
+    if (!open) return;
+    dialogRef.current?.focus();
+  }, [open]);
+
   const closeModal = useCallback(() => {
     setOpen(false);
     setUri(null);
+    triggerRef.current?.focus();
   }, []);
         <div
+          ref={dialogRef}
+          tabIndex={-1}
           className="bg-deep-black relative z-10 max-h-[90vh] w-full max-w-md overflow-y-auto rounded-xl border border-gray-700 p-4 shadow-xl"
           role="dialog"

Add ref={triggerRef} to the share <button> at Line 87, and import useRef.

As per coding guidelines: "Use function components only, treat react-hooks/exhaustive-deps violations as errors, use optional chaining in JSX, and give every interactive control an aria-label" and the accessibility rule that modals must be navigable.

🤖 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/ChatDmPaperControls.tsx` around lines 103 - 123,
Update the modal flow in ChatDmPaperControls to manage focus: create a ref for
the share trigger, attach it to the share button, move focus to the dialog’s
first usable control when opening, and restore focus to the trigger when
closing. Keep the existing Escape and busy behavior intact, and ensure any new
hook effects satisfy exhaustive-deps.

Source: Coding guidelines

src/shared/meshClientDeepLink.ts-224-230 (1)

224-230: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the intentional unlogged fallback.

Line 224 catches URL parsing failures and continues with raw paper-blob validation. Add a catch-no-log-ok comment that states this fallback handles malformed or long paper URIs.

As per coding guidelines, “Catch blocks must log, rethrow, or include // catch-no-log-ok <reason>.”

Proposed fix
     } catch {
+      // catch-no-log-ok: URL parsing can reject long paper blobs; validate the raw scheme payload.
       // Some engines reject very long hosts; still try paper when scheme + blob remain.
🤖 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/shared/meshClientDeepLink.ts` around lines 224 - 230, Add a `//
catch-no-log-ok` comment inside the catch block of the URI parsing flow,
explaining that malformed or overly long paper URIs intentionally fall back to
raw paper-blob validation without logging. Keep the existing fallback behavior
and return values unchanged.

Source: Coding guidelines

reticulum-sidecar/src/stack/live.rs-4240-4253 (1)

4240-4253: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Resolve the sender display name before returning the paper-ingest payload.

The payload returned here passes None for inbound_sender_name, so lxmf_payload_from_message falls back to the sender's truncated hex hash. The delivery callback that also fires for this same ingested message (see Line 352-365) resolves the sender name from self.display_name_cache via resolve_inbound_sender_name_map. As a result, the HTTP response and the WS-broadcast event for the same inbound paper message can carry different sender_name values for a known contact.

Resolve the name from display_name_cache here the same way the callback does, so both payloads agree.

🐛 Proposed fix to resolve the sender name consistently
+        let sender_hex = hex::encode(message.source_hash);
+        let inbound_sender_name = self
+            .display_name_cache
+            .lock()
+            .ok()
+            .map(|cache| resolve_inbound_sender_name_map(&cache, &sender_hex))
+            .unwrap_or_else(|| sender_hex.get(..12).unwrap_or(&sender_hex).to_string());
+
         let payload = lxmf_payload_from_message(
             &message,
             &self.lxmf_hash_hex,
             &self.display_name,
             Some("paper"),
             None,
             "inbound",
-            None,
+            Some(&inbound_sender_name),
         );
🤖 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 `@reticulum-sidecar/src/stack/live.rs` around lines 4240 - 4253, Update the
paper-ingest response construction around lxmf_payload_from_message to resolve
the inbound sender name from self.display_name_cache using the existing
resolve_inbound_sender_name_map behavior, then pass that resolved name instead
of None for inbound_sender_name. Keep the HTTP payload consistent with the
delivery callback and preserve the existing fallback for unknown contacts.
🧹 Nitpick comments (1)
src/renderer/lib/reticulum/createReticulumPaperMessage.test.ts (1)

116-133: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the rejected-proxy branch.

The tests cover sidecar error codes and an incomplete response. They do not cover a rejected proxyPost, which is the path that maps transport failures to chatPanel.shareAsPaperFailed. The sidecar proxy rejects when the sidecar is not running or the HTTP status is not ok, so this branch is reachable in normal use.

♻️ Proposed additional test
+  it('maps proxy rejection to the generic failure key', async () => {
+    proxyPost.mockRejectedValue(new Error('Reticulum sidecar is not running'));
+    const result = await createReticulumPaperMessage({
+      identityId: 'id-1',
+      destinationHash: 'bb'.repeat(16),
+      text: 'hi',
+    });
+    expect(result).toEqual({ ok: false, errorKey: 'chatPanel.shareAsPaperFailed' });
+    expect(ingestReticulumLxmfPayloadWithSideEffects).not.toHaveBeenCalled();
+  });
🤖 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/reticulum/createReticulumPaperMessage.test.ts` around lines
116 - 133, The tests for createReticulumPaperMessage cover sidecar errors and
incomplete responses but omit a rejected proxyPost transport failure. Add a test
alongside the existing cases that makes proxyPost reject, invokes
createReticulumPaperMessage with the standard request fields, and asserts
chatPanel.shareAsPaperFailed with no call to
ingestReticulumLxmfPayloadWithSideEffects.
🤖 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 `@scripts/check-i18n-quality.mjs`:
- Around line 2224-2228: Remove or revise LXM_SCHEME_GLUED_TO_WORD_RE and its
validation so valid lxm:// and lxma:// paper URIs with alphabetic payload
prefixes are accepted; if retaining detection, parse complete URI tokens before
checking surrounding prose. Add a regression case covering an alphabetic paper
payload prefix, while preserving BROKEN_LXM_SCHEME_RE handling for whitespace
before ://.

In `@src/renderer/components/ChatDmPaperControls.test.tsx`:
- Around line 132-136: Update the clipboard rejection test around
writeClipboardText.mockRejectedValueOnce to mock console warnings with
mockConsoleWarn from `@/renderer/lib/vitestConsoleMock`, assert the expected
warning is recorded after the failed share action, and restore the console
warning mock afterward.

In `@src/renderer/components/ChatPanel.tsx`:
- Around line 2345-2353: Update ChatPanelProps and the ChatPanel destructuring
near hasLxstVoice to include hasLxmfPaper, resolve it from ProtocolCapabilities
in the parent, and require it alongside protocol === 'reticulum' in both paper
control gates: paperShareControl at src/renderer/components/ChatPanel.tsx lines
2345-2353 and the second paper control at lines 3177-3179.

---

Other comments:
In `@reticulum-sidecar/src/stack/live.rs`:
- Around line 4240-4253: Update the paper-ingest response construction around
lxmf_payload_from_message to resolve the inbound sender name from
self.display_name_cache using the existing resolve_inbound_sender_name_map
behavior, then pass that resolved name instead of None for inbound_sender_name.
Keep the HTTP payload consistent with the delivery callback and preserve the
existing fallback for unknown contacts.

In `@src/renderer/components/ChatDmPaperControls.tsx`:
- Around line 103-123: Update the modal flow in ChatDmPaperControls to manage
focus: create a ref for the share trigger, attach it to the share button, move
focus to the dialog’s first usable control when opening, and restore focus to
the trigger when closing. Keep the existing Escape and busy behavior intact, and
ensure any new hook effects satisfy exhaustive-deps.

In `@src/renderer/components/ReticulumMessageStatusBadge.tsx`:
- Around line 110-118: Update the tooltip construction near statusLabelText and
viaPrefixText so the status suffix is omitted only when deliveryMethod is paper
and status represents completion. Failed paper messages must retain
`${viaPrefix}: ${statusLabel}`, including the error or failure text from
statusLabelText; preserve the existing paper-only prefix behavior for completed
messages.

In `@src/renderer/lib/reticulumVoiceCallTones.ts`:
- Around line 271-290: Update scheduleModemChirp so each one-shot oscillator and
gain node is removed from the modem tracking collections when the oscillator
emits its ended event, while preserving the existing tracking and teardown
behavior. Add the corresponding untracking support using the existing
trackModemStoppable and trackModemDisconnectable mechanisms, and update the test
mock in reticulumVoiceCallTones.test.ts if needed to expose ended handling.

In `@src/shared/meshClientDeepLink.ts`:
- Around line 224-230: Add a `// catch-no-log-ok` comment inside the catch block
of the URI parsing flow, explaining that malformed or overly long paper URIs
intentionally fall back to raw paper-blob validation without logging. Keep the
existing fallback behavior and return values unchanged.

---

Nitpick comments:
In `@src/renderer/lib/reticulum/createReticulumPaperMessage.test.ts`:
- Around line 116-133: The tests for createReticulumPaperMessage cover sidecar
errors and incomplete responses but omit a rejected proxyPost transport failure.
Add a test alongside the existing cases that makes proxyPost reject, invokes
createReticulumPaperMessage with the standard request fields, and asserts
chatPanel.shareAsPaperFailed with no call to
ingestReticulumLxmfPayloadWithSideEffects.
🪄 Autofix

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: 22f275ca-dee4-4f43-8c32-2d9b7b70e609

📥 Commits

Reviewing files that changed from the base of the PR and between 8fc779e and e044f2e.

⛔ 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 (52)
  • AGENTS.md
  • README.md
  • docs/reticulum-sidecar-ipc.md
  • docs/reticulum.md
  • docs/troubleshooting.md
  • reticulum-sidecar/src/api/lxmf.rs
  • reticulum-sidecar/src/api/mod.rs
  • reticulum-sidecar/src/stack/live.rs
  • reticulum-sidecar/src/stack/lxmf_delivery.rs
  • reticulum-sidecar/src/stack/lxmf_outbound.rs
  • reticulum-sidecar/src/stack/mod.rs
  • reticulum-sidecar/src/stack/types.rs
  • scripts/check-i18n-quality.mjs
  • src/main/database.test.ts
  • src/main/index.contract.test.ts
  • src/main/index.ts
  • src/main/ipc/reticulum-db-handlers.test.ts
  • src/main/ipc/reticulum-db-handlers.ts
  • src/renderer/components/ChatDmPaperControls.test.tsx
  • src/renderer/components/ChatDmPaperControls.tsx
  • src/renderer/components/ChatPanel.tsx
  • src/renderer/components/ReticulumMessageStatusBadge.test.tsx
  • src/renderer/components/ReticulumMessageStatusBadge.tsx
  • src/renderer/components/ReticulumNetworkPanel.test.tsx
  • src/renderer/components/ReticulumNetworkPanel.tsx
  • src/renderer/hooks/useMeshClientDeepLink.test.tsx
  • src/renderer/hooks/useMeshClientDeepLink.tsx
  • src/renderer/lib/ingest/reticulumIngest.ts
  • src/renderer/lib/meshClientDeepLinkApply.test.ts
  • src/renderer/lib/meshClientDeepLinkApply.ts
  • src/renderer/lib/reticulum/classifyReticulumVia.test.ts
  • src/renderer/lib/reticulum/classifyReticulumVia.ts
  • src/renderer/lib/reticulum/createReticulumPaperMessage.test.ts
  • src/renderer/lib/reticulum/createReticulumPaperMessage.ts
  • src/renderer/lib/reticulum/handleReticulumQrIngest.test.ts
  • src/renderer/lib/reticulum/handleReticulumQrIngest.ts
  • src/renderer/lib/reticulum/showReticulumQrIngestToast.ts
  • src/renderer/lib/reticulumVoiceCallTones.test.ts
  • src/renderer/lib/reticulumVoiceCallTones.ts
  • src/renderer/lib/reticulumVoiceSession.test.ts
  • src/renderer/lib/reticulumVoiceSession.ts
  • src/renderer/lib/storeRecordAdapters.test.ts
  • src/renderer/lib/storeRecordAdapters.ts
  • src/renderer/lib/types.ts
  • src/renderer/runtime/useReticulumRuntime.voice.test.ts
  • src/renderer/stores/messageStore.ts
  • src/shared/meshClientDeepLink.test.ts
  • src/shared/meshClientDeepLink.ts
  • src/shared/reticulumMessageTransport.test.ts
  • src/shared/reticulumMessageTransport.ts
  • src/shared/reticulumPaperErrors.test.ts
  • src/shared/reticulumPaperErrors.ts

Comment thread scripts/check-i18n-quality.mjs
Comment thread src/renderer/components/ChatDmPaperControls.test.tsx Outdated
Comment thread src/renderer/components/ChatPanel.tsx
Gate paper UI on hasLxmfPaper, resolve paper-ingest sender names, restore modal focus, and harden i18n/tones/tooltip/tests from review.
@rinchen
rinchen merged commit 5e5d321 into main Aug 5, 2026
21 checks passed
@rinchen
rinchen deleted the paper branch August 5, 2026 03:08
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