Skip to content

Agent reference: Chat Panel

Deep subsystem reference for AI assistants. Open this when a task touches the Chat panel, composer, link previews, notifications, dedup, hop badges, reactions/tapbacks, mentions, or chat/support export. Hard rules live in AGENTS.md.

  • Components: ChatPanel.tsx (channel/DM UI) + shared ChatComposer.tsx (drafts, mentions, chunking, spellcheck, emoji; also used by RoomsPanel.tsx). Reticulum DM Share as paper / Scan paper via ChatDmPaperControls.tsx + createReticulumPaperMessage.ts. Scroll-at-bottom helper: chatScrollUtils.ts (getDistFromChatBottom).
  • Composer limits / send cadence: chatComposerLimits.tsgetMaxChunks(protocol) via ProtocolCapabilities.composerMaxChunks (MeshCore = 1: no outbound [i/N] split; splitChatMessage returns null when text needs more than one packet), room payload via getMeshcoreRoomPayloadLimit, computeComposerLimitStatus phases (warn surfaces a single-packet ⓘ hint; overMaxSingle disables send and shows a role="note" callout). MeshCore also gets a non-blocking ~5s "sending too fast" advisory (role="status", dismissible) from an app-wide clock in meshcoreSendRateNotice.ts (recordMeshcoreSend / isMeshcoreSendTooFast, MESHCORE_FAST_SEND_WARN_INTERVAL_MS). Every live MeshCore send advances the clock and can show the advisory — text (handleSend), GIF, and share-location in ChatComposer, plus outbox drain (useChatOutbox.ts). Legacy MeshCore outbox rows with groupTotal > 1 or [i/N] payloads are quarantined (blocked) on drain instead of being transmitted. Inbound multi-part [i/N] merge is unchanged. i18n: chatPanel.composeLimit.meshcoreSingleNotice.*, chatPanel.meshcoreFastSend.warning, chatPanel.outboxLegacyMultipartBlocked. See meshcore-meshtastic-parity.md.
  • Payload / links: ChatPayloadText.tsx — mention highlighting, search marks, URL linkification; link previews via chat:fetchLinkPreview (src/main/fetchLinkPreview.ts): Open Graph for HTML pages; YouTube watch/shorts/youtu.be via oEmbed + thumbnail; direct image URLs (path extension via chatDirectImageUrl.ts or raster Content-Type) return kind: 'image' and render as inline embeds (ChatInlineImage / DirectImageEmbed); OG/YouTube use card layout. Security: DNS-pinned undici Agent, private/loopback blocked, magic-byte MIME sniff (safeRasterImageMime.ts), HTTPS-only image embeds, 10s fetch / 3s DNS, 64 KiB HTML cap, 2 MiB image fetch cap (256 KiB cache payload cap), LRU caches, single-flight dedup (renderer map capped). Previews load even when scrolled up. LXMF attachment rasters: chat:readReticulumAttachmentAsDataUrl (reticulum-attachment-image.ts; path jail, magic-byte MIME, SVG rejected, 2 MiB, IPC rate limit) → ReticulumAttachmentLine. LXMF voice memos (hasReticulumVoiceMemo): Chat DM mic → sidecar /api/v1/voice/memo/* via electronAPI.reticulum.voiceMemo.*; playback via chat:readReticulumAttachmentBytes (reticulum-attachment-audio.ts; OggS sniff, 256 KiB) → ReticulumVoiceMemoLine. Reply quotes: replyPreview.ts.
  • Storage helpers: src/renderer/lib/chatPanelProtocolStorage.ts — drafts (mesh-client:drafts:<protocol>), open DM tabs, last-read, per-view mute (mesh-client:mutedViews:<protocol>), starred (mesh-client:starred:<protocol>, cap 200), MeshCore flood-scope overrides per chat view (mesh-client:floodScopeOverrides:<protocol>, channel or DM viewKey).
  • Notifications: src/renderer/lib/chatNotifications.tsplayMessageNotification(type) via Web Audio: channel = single 880 Hz pulse (150 ms); dm / reply = dual pulse (587.33 Hz then 783.99 Hz, 50 ms each, 35 ms gap). Resumes suspended AudioContext when the window is hidden/minimized. Type selection in chatUnreadCounts.ts (resolveChatNotificationType, pickAudibleNotificationType; batch priority reply > dm > channel). ChatPanel plays when the user is on Chat but reading another view; App plays for other panels / backgrounded window (avoids double beep). Meshtastic hidden-window desktop notifications are visual-only (silent: true in meshtasticRouterSideEffects.ts); typed Web Audio from App owns sound. Global mute mesh-client:notifMuted; per-view mute in mutedViews. Main-process tray icon shows unread when chat or MeshCore Rooms traffic arrives while backgrounded (src/main/index.ts buildTrayIcon).
  • Meshtastic dedup: meshtasticMessageDedup.ts — merges delayed RF/MQTT duplicates (10-minute content window) in useMeshtasticRuntime ingest.
  • Hop badges: MessageRecord.rxHops / viaStoreForward round-trip via storeRecordAdapters.ts and meshtasticDbCacheHydration.ts (hopCount on MessageRecord bridges to rxHops for Meshtastic PacketRouter rows); Chat hop pills read ChatMessage.rxHops. MeshCore (primary): companion pathLen on events 7/8 → meshcoreCompanionRxPathLenToHopCountDomainEvent.payload.hopCount (MeshCoreProtocol, meshcoreDirectMessageDecode); waiting-message drain also sets rxHops from pathLen. MeshCore (fallback): raw-log correlation via resolveMeshcoreIngestRxHops / MESHCORE_CHAT_CORRELATE_WINDOW_MS (3000ms); rawPacketsRef synced inside event-136 setRawPackets updater for same-tick ingest (meshcoreConnSideEffects). Meshtastic: MeshtasticProtocol uses meshtasticComputedRfHopsAway — omit hops for viaMqtt and hopStart === 0; else hopStart - hopLimit when hopStart > 0 && hopLimit <= hopStart.
  • MeshCore dedup: meshcoreStoreDedup.ts — RF/MQTT merge, companion TX echo, tapback self-echo, room BBS paths in useMeshcoreRuntime ingest (5-minute cross-transport window; distinct from Meshtastic’s 10-minute window).
  • Reactions / tapbacks: reactions.ts (Meshtastic protobuf) + meshcoreChannelText.ts (MeshCore default outbound keyless @[Name] emoji / @[Name] body; opt-in MeshCore Open compatibility in App enables keyed replies, r: reactions, and g: GIF send via meshcoreOpenWireCompatEnabled in appSettingsStorage.ts; inbound keyed/sec↔ms parent match via meshcoreMessageMatchesReplyKey; inbound emoji-only replies promoted via meshcorePromoteEmojiOnlyReplyToTapback; inbound Open r:HASH:INDEX in meshcoreOpenReaction.ts); ChatPanel.tsx attaches tapbacks via replyId + runtime/panel sendReaction.
  • Mention segments: src/renderer/lib/chatMentionSegments.ts — parse/build @[Name] tokens; MentionAutocomplete.tsx renders the dropdown.
  • Export IPC: chat:export — renderer calls window.electronAPI.chat.export(messages); main opens a Save dialog and writes a .txt file.
  • Support bundle IPC: support:exportBundleexportSupportBundle.tswindow.electronAPI.support.exportBundle(mode, json); main support-bundle.ts writes zip (github = logs + debug snapshot including Reticulum diagnostic JSON and Meshtastic channel layout triage via debugSnapshotMeshtasticContext.ts; developer = SQLite plus redacted reticulum/config and reticulum/mesh_client_stack.json). Modes in support-bundle.types.ts.