Agent reference: Renderer hook architecture (multi-protocol)

August 21, 2026 · View on GitHub

Deep subsystem reference for AI assistants. Open this when a task touches renderer hooks/runtimes/stores, protocol entry points, identity hydration, the SQLite database layer, or tab/UI wiring. Hard rules live in AGENTS.md.

See Renderer: hooks vs runtime vs lib (layout map in AGENTS.md). Legacy useDevice / useMeshCore are removed (#375, #377). Default rules for new UI:

ConcernUse
Orchestration (App tab)useProtocolFacade(protocol) — connection, useConnectionView, panel bundle, nodes, messages
Active protocol identityuseActiveMeshIdentity(protocol) — focused identityId per tab; prefer capabilities over protocol ===
LoRa dual-protocol panel bundles (App)useAllProtocolPanelActions (Meshtastic + MeshCore + Reticulum); prefer this over per-protocol panel-action hooks at the App shell
Reads (nodes, messages, connection fields)Zustand stores + useNodes / useMessages / useConnectionView / useConnectionStatus
Writes (configure, send, admin, panel callbacks)usePanelActions(protocol, identityId, …) / useProtocolFacade(protocol).panel or useSendMessage(identityId)
Connect / disconnect / auto-connectuseProtocolConnectionActions(protocol) (useProtocolConnect + useProtocolDisconnect + lib/sessions/*Session.ts); Meshtastic/MeshCore via ConnectionDriver; Reticulum via sidecar start/stop. Launch RF auto-connect (serial/BLE/TCP/HTTP) via ProtocolAutoConnectCoordinator + useProtocolRfAutoConnect + protocolRfAutoConnectGate (cancelProtocolRfAutoConnect before manual Connect). LoRa reconnect single-owner: rfReconnectController
Wire subscriptions, MQTT IPC, reconnect, DB hydrationuseMeshtasticRuntime / useMeshcoreRuntime / useReticulumRuntime in runtime/ — mount once from App.tsx via context providers

Do not remount protocol runtimes in child components. Do not compare protocol === 'meshcore' for feature gates; use ProtocolCapabilities / useRadioProvider(protocol).

Protocol SDK adapters: src/renderer/lib/protocols/. Connection lifecycle: ConnectionDriver (lib/drivers/); inbound domain events: Protocol → PacketRouter → identity stores, then side-effect listeners (ingest already applied). Meshtastic post-router side effects: lib/ingest/meshtasticIngest.ts, meshtasticRouterSideEffects.ts (MQTT uplink / notifications / device_log), meshtasticNodeSideEffects.ts, meshtasticRawPacketSideEffects.ts, meshtasticTraceSideEffects.ts, meshtasticModulePortSideEffects.ts, meshtasticStoreForwardSideEffects.ts; meshtasticTransportSideEffects.ts handles transport-state cleanup, while lifecycle-only SDK attach remains in meshtasticRuntimeWireEffects.ts (DeviceStatus / MyNodeInfo / FromRadio / heartbeat / config / remote-admin — not a second packet decode path). MeshCore post-router side effects: lib/ingest/meshcoreIngest.ts (chat persist, last_heard, path-updated), hooks/meshcore/meshcoreConnSideEffects.ts + MeshcoreConnSideEffectsCtx (meshcoreConnSideEffectsCtx.ts) (DM ack 130, waiting drain 131, RF RX 136, CLI, disconnect), lib/meshcore/meshcoreLiveContactPersist.ts (SQLite contact rows), lib/meshcore/meshcorePubKeyRegistry.ts (DM/trace pubkeys). Live UI nodes/messages read nodeStore / messageStore via identityStoreReads (getIdentityNode / getIdentityChatMessages); runtimes do not keep hook-local node/message wire mirrors. Transport params / Protocol attach helpers: meshIdentityBridge. Favorites: setNodeFavorited patches meshcoreIdentityIdRef (fallback getIdentityIdForProtocol('meshcore')). Dedup windows: cross-transport and channel RF 5 min; room/tapback 60 s. Path-updated (129) for existing contacts does not bump SQLite last_advert until the next advert (128).

Identity-scoped UI stores: identityStore, nodeStore, messageStore, connectionStore — nodes/messages keyed by identityId. Ephemeral relay coverage: relayCoverageStore (in-memory only; identityId:messageId) for outbound Chat bubble fillers — writers in useSendMessage / protocol runtimes / heardRepeatTracker / meshtasticHeardRepeat / reticulumRouteCoverage; messageStore.renameMessageId re-keys coverage + MeshCore listen windows; cleared on identity disconnect / Reticulum session teardown. See chat.md “Relay coverage”. MQTT status bridge: mirrorMqttStatusToConnection copies main-process mqtt.onStatus IPC into connectionStore.mqttStatus from runtime handlers until MQTT moves fully into ConnectionDriver. SQLite → UI: lib/hydrateIdentityStoresFromDb.ts (coordinator: identityHydrationCoordinator.ts; Meshtastic node map: meshtasticDbCacheHydration.ts; message cap: meshtasticMessageLoadLimit.ts); manual refresh via hooks/useDbRefresh.ts. Identity-scoped Zustand hydration is the canonical UI path ([#375]). MeshCore contacts DB: meshcore_contacts.last_advert is Unix seconds; age prune uses src/shared/meshcoreContactAgeCutoff.ts (do not compare in ms).

Protocol entry points

  • Meshtastic: src/renderer/lib/protocols/MeshtasticProtocol.ts, useMeshtasticRuntime (side effects), src/renderer/lib/connection.ts (createConnection)
  • MeshCore: src/renderer/lib/protocols/MeshCoreProtocol.ts, useMeshcoreRuntime (side effects), @liamcottle/meshcore.js

Database

WAL SQLite; user_version in database.ts; migrations as migration_N(); db-compat.ts over node:sqlite. After schema changes: pnpm run check:db-migrations. Startup maintenance: lib/startupDbPrune.ts — single-flight per session from App.tsx (node/message retention, RF stub migration); do not re-invoke from unstable effect deps.

UI

Panels: src/renderer/components/. New tabs: lazyTabPanels.ts / lazyAppPanels.ts + capabilities. Tab visibility: src/renderer/lib/tabSlotIds.ts (TAB_SLOT_IDS) → src/renderer/lib/appTabMappings.ts (TAB_CAPABILITY_REQUIREMENTS, computeTabMappings() in App.tsx). Stores: module defaults; persist vs SQLite IPC as elsewhere. MeshCore Open wire / path-hash UI mounts from RadioPanel, which persists meshcoreOpenWireCompatEnabled and meshcorePathHashMode as app settings via mergeAppSetting (ownership stays on RadioPanel, not AppPanel).