Conversation
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.
|
Warning Review limit reached
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 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: QUIET Plan: Pro Plus Run ID: 📒 Files selected for processing (15)
📝 WalkthroughWalkthroughThe 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. ChangesReticulum paper transport
Outgoing voice-call tones
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
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 winRelease finished modem nodes instead of accumulating them for the whole connecting phase.
trackModemStoppableandtrackModemDisconnectableonly append. Nothing removes an entry when a one-shot node finishes. The handshake adds five entries. Each heartbeat chirp adds two more everyMODEM_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 toAudioNodeobjects that already finished. The later teardown loop then callsstop()on every finished node and relies on thecatchblocks.Untrack each one-shot node when it ends. The change is local to the chirp helper.
♻️ Proposed fix: untrack one-shot chirp nodes on
endedfunction 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()andctx.createGain(), soonendedis available. Keep the mock inreticulumVoiceCallTones.test.tsin 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 winRestrict the tooltip suffix omission to completed paper messages.
Line 118 drops
statusLabelfor every paper row, includingstatus="failed".statusLabelTextreturnserror ?? 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 winMove focus into the dialog on open and restore it on close.
The dialog renders with
role="dialog"andaria-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 todocument.♿ 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 importuseRef.As per coding guidelines: "Use function components only, treat
react-hooks/exhaustive-depsviolations as errors, use optional chaining in JSX, and give every interactive control anaria-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 winDocument the intentional unlogged fallback.
Line 224 catches URL parsing failures and continues with raw paper-blob validation. Add a
catch-no-log-okcomment 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 winResolve the sender display name before returning the paper-ingest payload.
The payload returned here passes
Noneforinbound_sender_name, solxmf_payload_from_messagefalls 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 fromself.display_name_cacheviaresolve_inbound_sender_name_map. As a result, the HTTP response and the WS-broadcast event for the same inbound paper message can carry differentsender_namevalues for a known contact.Resolve the name from
display_name_cachehere 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 winAdd 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 tochatPanel.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
⛔ Files ignored due to path filters (16)
src/renderer/locales/cs/translation.jsonis excluded by!src/renderer/locales/**src/renderer/locales/de/translation.jsonis excluded by!src/renderer/locales/**src/renderer/locales/en/translation.jsonis excluded by!src/renderer/locales/**src/renderer/locales/es/translation.jsonis excluded by!src/renderer/locales/**src/renderer/locales/fr/translation.jsonis excluded by!src/renderer/locales/**src/renderer/locales/id/translation.jsonis excluded by!src/renderer/locales/**src/renderer/locales/it/translation.jsonis excluded by!src/renderer/locales/**src/renderer/locales/ja/translation.jsonis excluded by!src/renderer/locales/**src/renderer/locales/ko/translation.jsonis excluded by!src/renderer/locales/**src/renderer/locales/nl/translation.jsonis excluded by!src/renderer/locales/**src/renderer/locales/pl/translation.jsonis excluded by!src/renderer/locales/**src/renderer/locales/pt-BR/translation.jsonis excluded by!src/renderer/locales/**src/renderer/locales/ru/translation.jsonis excluded by!src/renderer/locales/**src/renderer/locales/tr/translation.jsonis excluded by!src/renderer/locales/**src/renderer/locales/uk/translation.jsonis excluded by!src/renderer/locales/**src/renderer/locales/zh/translation.jsonis excluded by!src/renderer/locales/**
📒 Files selected for processing (52)
AGENTS.mdREADME.mddocs/reticulum-sidecar-ipc.mddocs/reticulum.mddocs/troubleshooting.mdreticulum-sidecar/src/api/lxmf.rsreticulum-sidecar/src/api/mod.rsreticulum-sidecar/src/stack/live.rsreticulum-sidecar/src/stack/lxmf_delivery.rsreticulum-sidecar/src/stack/lxmf_outbound.rsreticulum-sidecar/src/stack/mod.rsreticulum-sidecar/src/stack/types.rsscripts/check-i18n-quality.mjssrc/main/database.test.tssrc/main/index.contract.test.tssrc/main/index.tssrc/main/ipc/reticulum-db-handlers.test.tssrc/main/ipc/reticulum-db-handlers.tssrc/renderer/components/ChatDmPaperControls.test.tsxsrc/renderer/components/ChatDmPaperControls.tsxsrc/renderer/components/ChatPanel.tsxsrc/renderer/components/ReticulumMessageStatusBadge.test.tsxsrc/renderer/components/ReticulumMessageStatusBadge.tsxsrc/renderer/components/ReticulumNetworkPanel.test.tsxsrc/renderer/components/ReticulumNetworkPanel.tsxsrc/renderer/hooks/useMeshClientDeepLink.test.tsxsrc/renderer/hooks/useMeshClientDeepLink.tsxsrc/renderer/lib/ingest/reticulumIngest.tssrc/renderer/lib/meshClientDeepLinkApply.test.tssrc/renderer/lib/meshClientDeepLinkApply.tssrc/renderer/lib/reticulum/classifyReticulumVia.test.tssrc/renderer/lib/reticulum/classifyReticulumVia.tssrc/renderer/lib/reticulum/createReticulumPaperMessage.test.tssrc/renderer/lib/reticulum/createReticulumPaperMessage.tssrc/renderer/lib/reticulum/handleReticulumQrIngest.test.tssrc/renderer/lib/reticulum/handleReticulumQrIngest.tssrc/renderer/lib/reticulum/showReticulumQrIngestToast.tssrc/renderer/lib/reticulumVoiceCallTones.test.tssrc/renderer/lib/reticulumVoiceCallTones.tssrc/renderer/lib/reticulumVoiceSession.test.tssrc/renderer/lib/reticulumVoiceSession.tssrc/renderer/lib/storeRecordAdapters.test.tssrc/renderer/lib/storeRecordAdapters.tssrc/renderer/lib/types.tssrc/renderer/runtime/useReticulumRuntime.voice.test.tssrc/renderer/stores/messageStore.tssrc/shared/meshClientDeepLink.test.tssrc/shared/meshClientDeepLink.tssrc/shared/reticulumMessageTransport.test.tssrc/shared/reticulumMessageTransport.tssrc/shared/reticulumPaperErrors.test.tssrc/shared/reticulumPaperErrors.ts
Gate paper UI on hasLxmfPaper, resolve paper-ingest sender names, restore modal focus, and harden i18n/tones/tooltip/tests from review.
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 thereticulumLastSelfLxmfHashappSettings:setallowlist 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 URIPOST /api/v1/lxmf/paper/ingest— decrypt a paper URI into the local LXMF inboxinvalid_hash,paper_too_large,identity_not_configured,identity_unknown,invalid_uri,decrypt_failed,internal_error) normalized inmap_paper_create_error/map_paper_ingest_errordelivery_method: "paper"(nolxmf_outbound_statuspath)Renderer / shared
ChatDmPaperControls) — draft → QR/URI; scan/paste viaQrIngestControlhandleReticulumQrIngestfor paper + contact/identity QRlxmPaperMessagekind +looksLikeLxmPaperBlob(meshClientDeepLink.ts)lxm://paper deep links ingest without confirm (contacts / MeshCore still confirm)received_via/delivery_methodincludepaperreticulumMessageTransport+reticulumPaperErrorskeep IPC allowlists and i18n error maps aligned2. Paper harden (persist, helpers, i18n)
Follow-up fix so paper survives round-trips and errors stay actionable:
paperthrough SQLite IPC (reticulum-db-handlers) soreceived_via: paperis not stripped on save (badge survives restart)showReticulumQrIngestToastfor consistent success/error toastscheck-i18n-quality.mjs: detect locale scheme breakage (lxm:/// related) that plain key checks missedreticulum.md,reticulum-sidecar-ipc.md, troubleshooting table for paper create/ingest failuresChatDmPaperControls,createReticulumPaperMessage, transport/errors, DB handlers, deep-link apply3. LXST voice connect tones
Outbound call progress audio:
connecting(replaces post-DTMF auto-ringback)reticulumVoiceCallTones/ session tests4.
reticulumLastSelfLxmfHashallowlistSince #785, SQLite
appSettings:setrejected 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
61e3ae0482143ae2a80b0094e044f2ebTest plan
identity_unknowntoast; oversized text →paper_too_large; stack stopped / no identity → appropriate failuredecrypt_failedlxm://paper URI → ingests without confirm dialog; contact/lxma://still confirmreceived_viastill present in ChatreticulumLastSelfLxmfHashstill present in SQLiteapp_settings(not only localStorage)pnpm run check:i18n/ locale quality passes (scheme breakage rules)pnpm run check:reticulum-sidecar(or Clippy + paper API unit tests) when cargo availableSummary by CodeRabbit
lxm://links.