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) + sharedChatComposer.tsx(drafts, mentions, chunking, spellcheck, emoji; also used byRoomsPanel.tsx). Reticulum DM Share as paper / Scan paper viaChatDmPaperControls.tsx+createReticulumPaperMessage.ts. Scroll-at-bottom helper:chatScrollUtils.ts(getDistFromChatBottom). - Composer limits / send cadence:
chatComposerLimits.ts—getMaxChunks(protocol)viaProtocolCapabilities.composerMaxChunks(MeshCore = 1: no outbound[i/N]split;splitChatMessagereturnsnullwhen text needs more than one packet), room payload viagetMeshcoreRoomPayloadLimit,computeComposerLimitStatusphases (warnsurfaces a single-packet ⓘ hint;overMaxSingledisables send and shows arole="note"callout). MeshCore also gets a non-blocking ~5s "sending too fast" advisory (role="status", dismissible) from an app-wide clock inmeshcoreSendRateNotice.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 inChatComposer, plus outbox drain (useChatOutbox.ts). Legacy MeshCore outbox rows withgroupTotal > 1or[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. Seemeshcore-meshtastic-parity.md. - Payload / links:
ChatPayloadText.tsx— mention highlighting, search marks, URL linkification; link previews viachat:fetchLinkPreview(src/main/fetchLinkPreview.ts): Open Graph for HTML pages; YouTube watch/shorts/youtu.be via oEmbed + thumbnail; direct image URLs (path extension viachatDirectImageUrl.tsor rasterContent-Type) returnkind: 'image'and render as inline embeds (ChatInlineImage/DirectImageEmbed); OG/YouTube use card layout. Security: DNS-pinned undiciAgent, 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/*viaelectronAPI.reticulum.voiceMemo.*; playback viachat: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 DMviewKey), weather-marked senders (mesh-client:weatherMarkedSenders:<protocol>, cap 500). - Weather view (#1112):
src/renderer/lib/weatherPosts.ts(isWeatherPost: built-in patterns for °F/°C,60F now,forecast,Hi N/Lo N, JSON with"temp"/"hum"; user-marked senders; optional user regex capped at 200 chars, invalid → ignored with inline error;WeakMapcache keyed by message + config) andsrc/renderer/stores/weatherFilterStore.ts(weatherFilterHideInChannels/weatherFilterPatternin app settings, marked senders per protocol). ChatPanelviewMode === 'weather'shows weather posts of the selected channel (channel tabs stay active; composer still targets that channel), with a toolbar count and inline settings (chat/WeatherFilterSettings.tsx); the sender header has a Mark as weather sender toggle. When hide-in-channels is on,isHiddenWeatherPostkeeps those posts out of the channel view,computeChannelUnreadCounts, andresolveChatNotificationType(returnsnull), so badges and tones match what Chat shows. DMs are never filtered. Not shown for DM-only chat (Reticulum). - Notifications:
src/renderer/lib/chatNotifications.ts—playMessageNotification(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);mecp= repeated ascending triple pulse for ROUTINE;mecpSafety= short–long (dit–dah) pairs × 6 at 1175 Hz square (~4.4s) for SAFETY;mecpSiren= six-cycle siren (~5s, aligned with URGENT) for MAYDAY;mecpEas= 853+960 Hz EAS attention (~5s) for URGENT (mute-bypass viauseMecpAlertWatcher/mecpAlert.ts, notpickAudibleNotificationType). Resumes suspendedAudioContextwhen the window is hidden/minimized. Type selection inchatUnreadCounts.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). The App inactive-chat watchers also raise desktop notifications, but only while the window is inactive. They cover all four chat surfaces: Meshtastic, MeshCore, Reticulum LXMF Chat and RRC. The logic lives inchatDesktopNotifications.tsanddesktopNotification.ts. These notifications are visual-only (silent: true) and use the same mute gates as the sound. Each conversation is tagged<protocol>:<view>. Clicking one callsapp:focusWindowand opens the DM, channel or RRC room. Typed Web Audio from App owns the sound. There is no runtime-local notifier. App → Notifications → Notification tones configures presets, imported short audio, volume and preview per event (notificationSoundSettings.ts; SQLite keynotificationSounds). Main owns bounded per-event copies vianotificationSounds:*IPC; existing sounds are defaults and missing custom audio falls back to the original. Preview cancellation is separate from incoming playback. See../notification-sounds.mdfor platform survey and boundaries. Global mutemesh-client:notifMuted; per-view mute inmutedViews. Main-process tray icon shows unread when chat or MeshCore Rooms traffic arrives while backgrounded (src/main/index.tsbuildTrayIcon). MECP compose/export and red-dashed bubbles: seemecp.md. - Meshtastic dedup:
meshtasticMessageDedup.ts— merges delayed RF/MQTT duplicates (10-minute content window) inuseMeshtasticRuntimeingest. - Hop badges:
MessageRecord.rxHops/viaStoreForwardround-trip viastoreRecordAdapters.tsandmeshtasticDbCacheHydration.ts(hopCountonMessageRecordbridges torxHopsfor Meshtastic PacketRouter rows); Chat hop pills readChatMessage.rxHops. MeshCore (primary): companionpathLenon events 7/8 →meshcoreCompanionRxPathLenToHopCount→DomainEvent.payload.hopCount(MeshCoreProtocol,meshcoreDirectMessageDecode); waiting-message drain also setsrxHopsfrompathLen. MeshCore (fallback): raw-log correlation viaresolveMeshcoreIngestRxHops/MESHCORE_CHAT_CORRELATE_WINDOW_MS(3000ms);rawPacketsRefsynced inside event-136setRawPacketsupdater for same-tick ingest (meshcoreConnSideEffects). Meshtastic:MeshtasticProtocolusesmeshtasticComputedRfHopsAway— omit hops forviaMqttandhopStart === 0; elsehopStart - hopLimitwhenhopStart > 0 && hopLimit <= hopStart. - Bubbles and composer (v6, Option B): incoming bubbles use
bg-chat-incoming-bg/border-chat-incoming-border, own bubblesbg-chat-outgoing-bg/border-chat-outgoing-border(all four user-editable in App → Appearance → Colors;THEME_TOKEN_ALPHAinthemeColors.tsrenders the fills translucent). DMs use the same tones as channels (no purple).rounded-xlwith the small corner toward the sender (rounded-tl-smin,rounded-tr-smout), row max 80% (94% belowsm, no rem cap, so wide windows keep long messages on one line); MECP severity tones (mecpChatBubbleToneClasses) always win. Incoming rows have a 24px avatar gutter (data-chat-avatar): sender initials fromlib/senderInitials.ts, or the Reticulum LXMF face (own Reticulum messages keep the face in the header); compact-mode continuations get an empty spacer so a sender's run reads as one block. Incoming hops / S&F / transport sit at the end of the header line; only header-less compact continuations show them as a footer. Toolbar icons sit in onerole="group"("Conversation tools"). The composer looks the same for channels, DMs and Rooms; Send shows an icon plus its label (Queue / Send N parts stay words), and a keyboard hint line (chatPanel.composeHint, hidden on touch) gives way to the length counter. - Relay coverage (in-memory, outgoing bubbles only):
relayCoverageStorekeyed byidentityId:messageId(not persisted). UI:RelayCoverageLineinChatPanelstatus row viarelayCoverageMessageKey. MeshCore:openHeardRepeatWindowfrom ChatuseSendMessagechannel sends and runtime channel TX; duringMESHCORE_HEARD_REPEAT_WINDOW_MS(120s — large meshes often need ≥60s for multi-hop returns), every overheardGRP_TXTflood-path segment credits a forwarder (Repeater/Room when resolved viapubKeyMapRef/ pubkey prefix; otherwise synthetic hex as “additional unidentified”). Chat hops are never credited. Path-invariant payload id (meshCorePathInvariantPayloadId: CRC lookup + full type/payload equality) binds only from own-TX or empty-path channel echo (never from the first credited hop); once bound, mismatched floods are ignored — unbound windows credit all in-window forwarders (pre-#888 best-effort). Naming is reliable on 2/3-byte path modes; 1-byte is best-effort (collisions common). Only forwarders whose rebroadcast reaches our radio are credited (multi-hop peers appear via accumulated path hashes).renameMessageIdre-keys coverage + window; disconnect clears window + identity coverage. Meshtastic: binary heard/timeout for RF channel/broadcast viameshtasticHeardRepeat+useMeshtasticRuntimetransport status (DMs/MQTT ignored). Reticulum: predicted route at send (reticulumRouteCoverage/sendReticulumChatMessage); hops-only vs via-first-hop i18n; pending→hash rename re-keys;clearReticulumSessionStoresstrips reticulum coverage. i18n:chatPanel.heardBy*,chatPanel.route*Predicted*. - MeshCore dedup:
meshcoreStoreDedup.ts— RF/MQTT merge, companion TX echo, tapback self-echo, room BBS paths inuseMeshcoreRuntimeingest (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, andg:GIF send viameshcoreOpenWireCompatEnabledinappSettingsStorage.ts; inbound keyed/sec↔ms parent match viameshcoreMessageMatchesReplyKey; inbound emoji-only replies promoted viameshcorePromoteEmojiOnlyReplyToTapback; inbound Openr:HASH:INDEXinmeshcoreOpenReaction.ts);ChatPanel.tsxattaches tapbacks viareplyId+ runtime/panelsendReaction. - Mention segments:
src/renderer/lib/chatMentionSegments.ts— parse/build@[Name]tokens;MentionAutocomplete.tsxrenders the dropdown. - Export IPC:
chat:export— renderer callswindow.electronAPI.chat.export(messages); main opens a Save dialog and writes a.txtfile. - Support bundle IPC:
support:exportBundle—exportSupportBundle.ts→window.electronAPI.support.exportBundle(mode, json); mainsupport-bundle.tswrites zip (github= logs + debug snapshot including Reticulum diagnostic JSON and Meshtastic channel layout triage viadebugSnapshotMeshtasticContext.ts;developer= SQLite plus redactedreticulum/configandreticulum/mesh_client_stack.json). Modes insupport-bundle.types.ts.