Ship Meshtastic Chat UX, embedded Infonet/SHELL panels, and Docker dev polish.

Rename Mesh Chat to Meshtastic Chat, embed the Infonet terminal with Arti/Tor warmup, improve the agent shell PTY (git in the backend image, operator PATH), and add docker-compose.override for local image builds. Gitignore Hermes Agent runtime installs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
BigBodyCobain
2026-06-11 00:55:38 -06:00
co-authored by Cursor
parent d1e1be4016
commit e78e4d186d
24 changed files with 1258 additions and 970 deletions
@@ -137,6 +137,9 @@ interface CommandHistory {
interface InfonetShellProps {
isOpen: boolean;
embedded?: boolean;
launchGate?: string;
onLaunchGateConsumed?: () => void;
onClose: () => void;
onOpenLiveGate?: (gate: string) => void;
onOpenDeadDrop?: (peerId: string, options?: { showSas?: boolean }) => void;
@@ -144,6 +147,9 @@ interface InfonetShellProps {
export default function InfonetShell({
isOpen,
embedded = false,
launchGate = '',
onLaunchGateConsumed,
onClose,
onOpenLiveGate,
onOpenDeadDrop,
@@ -176,6 +182,7 @@ export default function InfonetShell({
const inputRef = useRef<HTMLInputElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const gateLaunchAttemptRef = useRef(0);
const launchGateConsumedRef = useRef('');
// Real mesh identity
const nodeIdentity = useMemo(() => getNodeIdentity(), []);
@@ -203,6 +210,7 @@ export default function InfonetShell({
setPendingGate(null);
setInput('');
gateLaunchAttemptRef.current += 1;
launchGateConsumedRef.current = '';
setIsBooting(true);
setBootText([]);
@@ -600,6 +608,14 @@ export default function InfonetShell({
setHistory(prev => [...prev, { command: cmd, output }]);
};
useEffect(() => {
const gate = String(launchGate || '').trim().toLowerCase();
if (!isOpen || isBooting || !gate || launchGateConsumedRef.current === gate) return;
launchGateConsumedRef.current = gate;
handleCommand(`join ${gate}`);
onLaunchGateConsumed?.();
}, [isOpen, isBooting, launchGate, onLaunchGateConsumed]);
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
if (inputMode === 'normal' && input.startsWith('g/') && searchMatch) {
@@ -635,7 +651,12 @@ export default function InfonetShell({
}
return (
<div ref={containerRef} className="h-full bg-[#0a0a0a] text-gray-300 p-4 md:p-8 font-mono relative flex flex-col overflow-hidden">
<div
ref={containerRef}
className={`h-full min-h-0 bg-[#0a0a0a] text-gray-300 font-mono relative flex flex-col overflow-hidden ${
embedded ? 'p-2 md:p-3 text-[13px]' : 'p-4 md:p-8'
}`}
>
{currentView === 'terminal' && (
<>
{/* Top Navigation / Quick Launch */}
@@ -13,11 +13,12 @@ interface Stats {
seedPeers: number;
nodeEnabled: boolean;
syncOutcome: string;
syncError: string;
}
const EMPTY: Stats = {
meshtastic: 0, aprs: 0, ledgerNodes: 0, infonetEvents: 0,
syncPeers: 0, seedPeers: 0, nodeEnabled: false, syncOutcome: 'offline',
syncPeers: 0, seedPeers: 0, nodeEnabled: false, syncOutcome: 'offline', syncError: '',
};
export default function NetworkStats() {
@@ -41,6 +42,7 @@ export default function NetworkStats() {
?? infonet?.bootstrap?.default_sync_peer_count
?? 0,
);
const syncOutcome = String(infonet?.sync_runtime?.last_outcome || 'offline').toLowerCase();
setStats({
meshtastic: Number(channelsRes?.total_live || channelsRes?.total_nodes || meshRes?.signal_counts?.meshtastic || 0),
aprs: Number(meshRes?.signal_counts?.aprs || 0),
@@ -49,26 +51,36 @@ export default function NetworkStats() {
syncPeers: syncPeerCount,
seedPeers: seedPeerCount,
nodeEnabled: Boolean(infonet?.node_enabled),
syncOutcome: String(infonet?.sync_runtime?.last_outcome || 'offline').toLowerCase(),
syncOutcome,
syncError: String(infonet?.sync_runtime?.last_error || '').trim(),
});
} catch { /* ignore */ }
};
poll();
const interval = setInterval(poll, 15000);
const interval = setInterval(poll, 8000);
return () => { alive = false; clearInterval(interval); };
}, []);
const nodeColor = stats.syncOutcome === 'ok' ? 'text-green-400'
const syncErrorLower = stats.syncError.toLowerCase();
const artiBlocked = syncErrorLower.includes('arti') || syncErrorLower.includes('onion');
const nodeColor = stats.syncOutcome === 'ok' || stats.syncOutcome === 'solo' ? 'text-green-400'
: stats.syncOutcome === 'running' ? 'text-amber-400'
: stats.nodeEnabled ? 'text-amber-400' : 'text-gray-600';
const nodeLabel = stats.syncOutcome === 'ok' ? 'SEED SYNCED'
: stats.syncOutcome === 'solo' ? 'SOLO'
: stats.syncOutcome === 'running' ? 'SYNCING'
: stats.syncOutcome === 'error' || stats.syncOutcome === 'fork' ? 'RETRYING'
: stats.syncOutcome === 'error' || stats.syncOutcome === 'fork'
? (artiBlocked ? 'ARTI WARMING' : 'SYNC BACKOFF')
: stats.nodeEnabled ? 'WAITING' : 'OFFLINE';
const nodeTitle = stats.syncError
? `Infonet seed sync: ${stats.syncError}`
: stats.nodeEnabled
? 'Participant node enabled; waiting for seed ledger sync.'
: 'Participant node offline.';
return (
<div className="flex flex-wrap items-center justify-center gap-x-5 gap-y-1 mt-5 text-sm font-mono text-gray-500">
<span>NODE <span className={nodeColor}>{nodeLabel}</span></span>
<span title={nodeTitle}>NODE <span className={nodeColor}>{nodeLabel}</span></span>
<span className="text-gray-700">|</span>
<span>MESH <span className={stats.meshtastic > 0 ? 'text-green-400' : 'text-gray-600'}>{stats.meshtastic.toLocaleString()}</span></span>
<span className="text-gray-700">|</span>
@@ -4,9 +4,7 @@ import React, { useEffect } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import { X } from 'lucide-react';
import {
fetchInfonetNodeStatusSnapshot,
setInfonetNodeEnabled,
startTorHiddenService,
ensureInfonetParticipantNodeReady,
} from '@/mesh/controlPlaneStatusClient';
import InfonetShell from './InfonetShell';
@@ -39,16 +37,8 @@ export default function InfonetTerminal({
const connectParticipantNode = async () => {
try {
const nodeStatus = await fetchInfonetNodeStatusSnapshot(true).catch(() => null);
if (cancelled || nodeStatus?.node_enabled) return;
const torStatus = await startTorHiddenService().catch(() => null);
if (cancelled || !torStatus?.running || !torStatus?.onion_address) return;
await setInfonetNodeEnabled(true);
if (!cancelled) {
await fetchInfonetNodeStatusSnapshot(true).catch(() => null);
}
if (cancelled) return;
await ensureInfonetParticipantNodeReady();
} catch {
// Remote/shared viewers may not have local-operator rights. Leave manual controls intact.
}