mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-07-05 20:08:00 +02:00
Replace mock Agent Shell overlay with inline xterm PTY and dock/expand UX.
Uses a local-operator WebSocket bash session, keeps the map interactive, and SNAP docks the shell back into Mesh Chat instead of a floating blurred panel. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,281 +1,232 @@
|
||||
'use client';
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { GripHorizontal, Terminal } from 'lucide-react';
|
||||
import { Terminal } from 'lucide-react';
|
||||
import { Terminal as XTerm } from '@xterm/xterm';
|
||||
import { FitAddon } from '@xterm/addon-fit';
|
||||
import '@xterm/xterm/css/xterm.css';
|
||||
import { resolveAgentShellWsUrl } from '@/lib/agentShellWs';
|
||||
|
||||
const STORAGE_KEY = 'sb_agent_shell_dims';
|
||||
const SHELL_FONT_PX = 14;
|
||||
const MIN_SHELL_WIDTH = 300;
|
||||
const MIN_SHELL_HEIGHT = 220;
|
||||
const STRETCH_WIDTH_RATIO = 2.15;
|
||||
const STRETCH_MIN_WIDTH = 520;
|
||||
|
||||
type ShellSize = { w: number; h: number };
|
||||
|
||||
function readStoredSize(): ShellSize | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
try {
|
||||
const raw = window.localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw) as ShellSize;
|
||||
if (
|
||||
typeof parsed?.w === 'number' &&
|
||||
typeof parsed?.h === 'number' &&
|
||||
parsed.w >= MIN_SHELL_WIDTH &&
|
||||
parsed.h >= MIN_SHELL_HEIGHT
|
||||
) {
|
||||
return parsed;
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function writeStoredSize(size: ShellSize) {
|
||||
if (typeof window === 'undefined') return;
|
||||
try {
|
||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(size));
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
function clampSize(size: ShellSize, anchorLeft: number): ShellSize {
|
||||
const maxW = Math.max(MIN_SHELL_WIDTH, window.innerWidth - anchorLeft - 12);
|
||||
const maxH = Math.max(MIN_SHELL_HEIGHT, window.innerHeight - 12);
|
||||
return {
|
||||
w: Math.min(Math.max(size.w, MIN_SHELL_WIDTH), maxW),
|
||||
h: Math.min(Math.max(size.h, MIN_SHELL_HEIGHT), maxH),
|
||||
};
|
||||
}
|
||||
|
||||
function defaultStretchedSize(anchor: DOMRect): ShellSize {
|
||||
const stretchedW = Math.max(anchor.width * STRETCH_WIDTH_RATIO, STRETCH_MIN_WIDTH);
|
||||
return clampSize({ w: stretchedW, h: anchor.height }, anchor.left);
|
||||
}
|
||||
const CWD_STORAGE_KEY = 'sb_agent_shell_cwd';
|
||||
|
||||
type Props = {
|
||||
anchorRef: React.RefObject<HTMLElement | null>;
|
||||
active: boolean;
|
||||
expanded: boolean;
|
||||
onExpandedChange: (expanded: boolean) => void;
|
||||
};
|
||||
|
||||
export default function AgentShellPanel({ anchorRef, active }: Props) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [anchorRect, setAnchorRect] = useState<DOMRect | null>(null);
|
||||
const [size, setSize] = useState<ShellSize>({ w: STRETCH_MIN_WIDTH, h: 360 });
|
||||
const [pos, setPos] = useState({ x: 0, y: 0 });
|
||||
const [userResized, setUserResized] = useState(Boolean(readStoredSize()));
|
||||
const resizeRef = useRef<{
|
||||
edge: 'e' | 's' | 'se';
|
||||
startX: number;
|
||||
startY: number;
|
||||
origW: number;
|
||||
origH: number;
|
||||
} | null>(null);
|
||||
function readStoredCwd(): string {
|
||||
if (typeof window === 'undefined') return '';
|
||||
try {
|
||||
return window.localStorage.getItem(CWD_STORAGE_KEY) || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
export default function AgentShellPanel({ active, expanded, onExpandedChange }: Props) {
|
||||
const hostRef = useRef<HTMLDivElement>(null);
|
||||
const termRef = useRef<XTerm | null>(null);
|
||||
const fitRef = useRef<FitAddon | null>(null);
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const [status, setStatus] = useState<'idle' | 'connecting' | 'open' | 'error'>('idle');
|
||||
const [statusDetail, setStatusDetail] = useState('');
|
||||
const [cwd, setCwd] = useState('');
|
||||
|
||||
const disconnect = useCallback(() => {
|
||||
wsRef.current?.close();
|
||||
wsRef.current = null;
|
||||
termRef.current?.dispose();
|
||||
termRef.current = null;
|
||||
fitRef.current = null;
|
||||
setStatus('idle');
|
||||
}, []);
|
||||
|
||||
const measureAnchor = useCallback(() => {
|
||||
const el = anchorRef.current;
|
||||
if (!el) return;
|
||||
const rect = el.getBoundingClientRect();
|
||||
setAnchorRect(rect);
|
||||
setPos({ x: rect.left, y: rect.top });
|
||||
const fitTerminal = useCallback(() => {
|
||||
const fit = fitRef.current;
|
||||
const term = termRef.current;
|
||||
const ws = wsRef.current;
|
||||
if (!fit || !term) return;
|
||||
fit.fit();
|
||||
if (ws?.readyState === WebSocket.OPEN) {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'resize',
|
||||
cols: term.cols,
|
||||
rows: term.rows,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!userResized) {
|
||||
setSize(defaultStretchedSize(rect));
|
||||
const connect = useCallback(() => {
|
||||
if (!active || !hostRef.current) return;
|
||||
disconnect();
|
||||
|
||||
const term = new XTerm({
|
||||
fontFamily: 'var(--font-roboto-mono), ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
|
||||
fontSize: SHELL_FONT_PX,
|
||||
lineHeight: 1.35,
|
||||
cursorBlink: true,
|
||||
theme: {
|
||||
background: '#04070b',
|
||||
foreground: '#d9f7ff',
|
||||
cursor: '#22d3ee',
|
||||
selectionBackground: '#0e7490',
|
||||
},
|
||||
scrollback: 5000,
|
||||
});
|
||||
const fit = new FitAddon();
|
||||
term.loadAddon(fit);
|
||||
term.open(hostRef.current);
|
||||
fit.fit();
|
||||
termRef.current = term;
|
||||
fitRef.current = fit;
|
||||
|
||||
const storedCwd = readStoredCwd();
|
||||
setCwd(storedCwd);
|
||||
setStatus('connecting');
|
||||
setStatusDetail('');
|
||||
|
||||
const ws = new WebSocket(resolveAgentShellWsUrl(storedCwd));
|
||||
ws.binaryType = 'arraybuffer';
|
||||
wsRef.current = ws;
|
||||
|
||||
ws.onopen = () => {
|
||||
setStatus('open');
|
||||
fit.fit();
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'resize',
|
||||
cols: term.cols,
|
||||
rows: term.rows,
|
||||
}),
|
||||
);
|
||||
term.focus();
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
if (typeof event.data === 'string') {
|
||||
try {
|
||||
const payload = JSON.parse(event.data) as { type?: string; message?: string };
|
||||
if (payload.type === 'error') {
|
||||
setStatus('error');
|
||||
setStatusDetail(payload.message || 'Shell unavailable');
|
||||
term.writeln(`\r\n\x1b[31m${payload.message || 'Shell unavailable'}\x1b[0m`);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
term.write(event.data);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (event.data instanceof ArrayBuffer) {
|
||||
term.write(new Uint8Array(event.data));
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
setStatus('error');
|
||||
setStatusDetail('Could not connect to the local agent shell endpoint.');
|
||||
term.writeln('\r\n\x1b[31mCould not connect to the local agent shell endpoint.\x1b[0m');
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
setStatus((prev) => (prev === 'error' ? prev : 'idle'));
|
||||
term.writeln('\r\n\x1b[90m[session closed]\x1b[0m');
|
||||
};
|
||||
|
||||
term.onData((data) => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(new TextEncoder().encode(data));
|
||||
}
|
||||
});
|
||||
}, [active, disconnect]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!active) {
|
||||
disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
const stored = readStoredSize();
|
||||
if (stored) {
|
||||
setSize(clampSize(stored, rect.left));
|
||||
} else {
|
||||
setSize(defaultStretchedSize(rect));
|
||||
}
|
||||
}, [anchorRef, userResized]);
|
||||
connect();
|
||||
return () => disconnect();
|
||||
}, [active, connect, disconnect]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!active) return;
|
||||
measureAnchor();
|
||||
|
||||
const el = anchorRef.current;
|
||||
if (!el) return;
|
||||
|
||||
const observer = new ResizeObserver(() => measureAnchor());
|
||||
observer.observe(el);
|
||||
|
||||
const onWindowChange = () => measureAnchor();
|
||||
window.addEventListener('resize', onWindowChange);
|
||||
window.addEventListener('scroll', onWindowChange, true);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
window.removeEventListener('resize', onWindowChange);
|
||||
window.removeEventListener('scroll', onWindowChange, true);
|
||||
};
|
||||
}, [active, anchorRef, measureAnchor]);
|
||||
const timer = window.setTimeout(() => fitTerminal(), expanded ? 220 : 0);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [active, expanded, fitTerminal]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!active || userResized) return;
|
||||
const el = anchorRef.current;
|
||||
if (!el) return;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const base = { w: rect.width, h: rect.height };
|
||||
setSize(base);
|
||||
setPos({ x: rect.left, y: rect.top });
|
||||
if (!active) return;
|
||||
const onResize = () => fitTerminal();
|
||||
window.addEventListener('resize', onResize);
|
||||
return () => window.removeEventListener('resize', onResize);
|
||||
}, [active, fitTerminal]);
|
||||
|
||||
const frame = window.requestAnimationFrame(() => {
|
||||
setSize(defaultStretchedSize(rect));
|
||||
});
|
||||
return () => window.cancelAnimationFrame(frame);
|
||||
}, [active, anchorRef, userResized]);
|
||||
|
||||
const beginResize = (edge: 'e' | 's' | 'se') => (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
resizeRef.current = {
|
||||
edge,
|
||||
startX: event.clientX,
|
||||
startY: event.clientY,
|
||||
origW: size.w,
|
||||
origH: size.h,
|
||||
};
|
||||
|
||||
const onMove = (ev: MouseEvent) => {
|
||||
if (!resizeRef.current) return;
|
||||
const dx = ev.clientX - resizeRef.current.startX;
|
||||
const dy = ev.clientY - resizeRef.current.startY;
|
||||
const { edge: ed, origW, origH } = resizeRef.current;
|
||||
const anchorLeft = anchorRef.current?.getBoundingClientRect().left ?? pos.x;
|
||||
const next: ShellSize = {
|
||||
w: ed === 's' ? origW : origW + dx,
|
||||
h: ed === 'e' ? origH : origH + dy,
|
||||
};
|
||||
setUserResized(true);
|
||||
setSize(clampSize(next, anchorLeft));
|
||||
};
|
||||
|
||||
const onUp = () => {
|
||||
resizeRef.current = null;
|
||||
window.removeEventListener('mousemove', onMove);
|
||||
window.removeEventListener('mouseup', onUp);
|
||||
setSize((current) => {
|
||||
writeStoredSize(current);
|
||||
return current;
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('mousemove', onMove);
|
||||
window.addEventListener('mouseup', onUp);
|
||||
};
|
||||
|
||||
const snapToStretchedDefault = () => {
|
||||
const el = anchorRef.current;
|
||||
if (!el) return;
|
||||
const rect = el.getBoundingClientRect();
|
||||
setUserResized(false);
|
||||
try {
|
||||
window.localStorage.removeItem(STORAGE_KEY);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
setPos({ x: rect.left, y: rect.top });
|
||||
setSize(defaultStretchedSize(rect));
|
||||
};
|
||||
|
||||
if (!mounted || !active || !anchorRect) {
|
||||
if (!active) {
|
||||
return (
|
||||
<div className="flex-1 min-h-0 flex flex-col items-center justify-center px-4 py-6 text-center border-l-2 border-cyan-800/20">
|
||||
<Terminal size={18} className="text-cyan-400 mb-2" />
|
||||
<div className="text-sm font-mono tracking-[0.2em] text-cyan-300">AGENT SHELL</div>
|
||||
<Terminal size={16} className="text-cyan-400 mb-2" />
|
||||
<div className="text-sm font-mono tracking-[0.2em] text-cyan-300">LOCAL SHELL</div>
|
||||
<div className="mt-2 text-[13px] font-mono text-[var(--text-secondary)] leading-relaxed">
|
||||
Expand Mesh Chat to open the local agent shell.
|
||||
Expand Mesh Chat to open your operator shell.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const shell = (
|
||||
<div
|
||||
className="pointer-events-auto z-[250] flex flex-col border border-cyan-800/50 bg-[#05080c]/96 shadow-[0_18px_60px_rgba(0,0,0,0.55),0_0_0_1px_rgba(34,211,238,0.08)] backdrop-blur-sm"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: pos.x,
|
||||
top: pos.y,
|
||||
width: size.w,
|
||||
height: size.h,
|
||||
transition: userResized ? undefined : 'width 180ms ease-out',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 border-b border-cyan-900/40 px-3 py-2 shrink-0 select-none">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<Terminal size={13} className="text-cyan-400 shrink-0" />
|
||||
<span className="text-[13px] font-mono tracking-[0.18em] text-cyan-300">AGENT SHELL</span>
|
||||
<span className="hidden sm:inline text-[12px] font-mono text-slate-500 truncate">
|
||||
local CLI · user cwd
|
||||
</span>
|
||||
return (
|
||||
<div className="flex-1 min-h-0 flex flex-col border-l-2 border-cyan-800/25 bg-[#04070b]">
|
||||
<div className="flex items-center justify-between gap-2 border-b border-cyan-900/40 px-2 py-1.5 shrink-0">
|
||||
<div className="min-w-0 text-[12px] font-mono tracking-[0.14em] text-cyan-300/90 truncate">
|
||||
{cwd ? cwd : 'operator shell'}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
{!expanded ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onExpandedChange(true)}
|
||||
className="px-2 py-0.5 text-[11px] font-mono tracking-[0.12em] text-cyan-300 border border-cyan-800/40 hover:bg-cyan-950/30"
|
||||
>
|
||||
EXPAND
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onExpandedChange(false)}
|
||||
className="px-2 py-0.5 text-[11px] font-mono tracking-[0.12em] text-cyan-300 border border-cyan-800/40 hover:bg-cyan-950/30"
|
||||
>
|
||||
SNAP
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={snapToStretchedDefault}
|
||||
className="px-2 py-1 text-[12px] font-mono tracking-[0.14em] text-cyan-300/80 border border-cyan-800/40 hover:bg-cyan-950/30 transition-colors"
|
||||
title="Reset size to default stretch from Mesh Chat panel"
|
||||
onClick={connect}
|
||||
className="px-2 py-0.5 text-[11px] font-mono tracking-[0.12em] text-slate-400 border border-slate-700/40 hover:bg-white/5"
|
||||
>
|
||||
SNAP
|
||||
RECONNECT
|
||||
</button>
|
||||
<GripHorizontal size={14} className="text-cyan-600/60" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex-1 min-h-0 overflow-auto styled-scrollbar px-3 py-2 font-mono text-cyan-100/90"
|
||||
style={{ fontSize: SHELL_FONT_PX, lineHeight: 1.55 }}
|
||||
>
|
||||
<div className="text-slate-400">ShadowBroker agent shell (PTY wiring next)</div>
|
||||
<div className="text-slate-500 mt-1">Working directory: set your own path in Settings.</div>
|
||||
<div className="mt-3 text-emerald-300/90">$ openclaw</div>
|
||||
<div className="text-slate-500">$ codex</div>
|
||||
<div className="text-slate-500">$ gemini</div>
|
||||
<div className="mt-3 text-cyan-300/80 animate-pulse">█</div>
|
||||
</div>
|
||||
{status === 'error' && statusDetail && (
|
||||
<div className="px-2 py-1 text-[12px] font-mono text-amber-300/90 border-b border-amber-900/30 bg-amber-950/10 shrink-0">
|
||||
{statusDetail}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="border-t border-cyan-900/30 px-3 py-1.5 text-[12px] font-mono text-slate-500 shrink-0">
|
||||
Drag right/bottom edges to resize · {Math.round(size.w)}×{Math.round(size.h)}px · {SHELL_FONT_PX}px font
|
||||
</div>
|
||||
<div ref={hostRef} className="flex-1 min-h-[220px] px-1 py-1 overflow-hidden" />
|
||||
|
||||
<div
|
||||
className="absolute top-2 bottom-2 right-0 w-1.5 cursor-e-resize"
|
||||
onMouseDown={beginResize('e')}
|
||||
aria-hidden
|
||||
/>
|
||||
<div
|
||||
className="absolute left-2 right-2 bottom-0 h-1.5 cursor-s-resize"
|
||||
onMouseDown={beginResize('s')}
|
||||
aria-hidden
|
||||
/>
|
||||
<div
|
||||
className="absolute right-0 bottom-0 h-3 w-3 cursor-se-resize"
|
||||
onMouseDown={beginResize('se')}
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="border-t border-cyan-900/30 px-2 py-1 text-[11px] font-mono text-slate-500 shrink-0">
|
||||
{status === 'connecting'
|
||||
? 'Connecting…'
|
||||
: status === 'open'
|
||||
? `${expanded ? 'Expanded' : 'Docked'} · ${SHELL_FONT_PX}px · map stays interactive`
|
||||
: 'Local operator shell'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex-1 min-h-0 flex flex-col items-center justify-center px-3 py-4 text-center border-l-2 border-cyan-800/20">
|
||||
<div className="text-[12px] font-mono tracking-[0.16em] text-cyan-500/80">SHELL ACTIVE</div>
|
||||
<div className="mt-1 text-[13px] font-mono text-[var(--text-secondary)] leading-relaxed">
|
||||
Panel stretched from Mesh Chat. Drag edges on the shell to resize.
|
||||
</div>
|
||||
</div>
|
||||
{createPortal(shell, document.body)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import AgentShellPanel from './AgentShellPanel';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Radio,
|
||||
Shield,
|
||||
Terminal,
|
||||
SquareTerminal,
|
||||
UserPlus,
|
||||
Lock,
|
||||
Check,
|
||||
@@ -91,6 +92,7 @@ function describeGateCompatReason(reason: string, gateId: string): string {
|
||||
|
||||
const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
|
||||
const panelBoxRef = useRef<HTMLDivElement>(null);
|
||||
const [shellExpanded, setShellExpanded] = useState(false);
|
||||
const ctrl = useMeshChatController(props);
|
||||
const {
|
||||
// UI state
|
||||
@@ -319,6 +321,12 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
|
||||
recentPrivateFallbackReason,
|
||||
onSettingsClick,
|
||||
} = ctrl;
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab !== 'dms') {
|
||||
setShellExpanded(false);
|
||||
}
|
||||
}, [activeTab]);
|
||||
const selectedContactTrustSummary = selectedContactInfo
|
||||
? getContactTrustSummary(selectedContactInfo)
|
||||
: null;
|
||||
@@ -401,7 +409,11 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
|
||||
{/* Single unified box — matches Data Layers panel skin */}
|
||||
<div
|
||||
ref={panelBoxRef}
|
||||
className={`bg-[#0a0a0a]/90 backdrop-blur-sm border border-cyan-900/40 flex flex-col relative overflow-hidden`}
|
||||
className={`bg-[#0a0a0a]/90 backdrop-blur-sm border border-cyan-900/40 flex flex-col relative overflow-hidden transition-[width,min-width] duration-200 ${
|
||||
activeTab === 'dms' && shellExpanded
|
||||
? 'z-[210] min-w-[min(760px,calc(100vw-3rem))] w-[min(760px,calc(100vw-3rem))]'
|
||||
: ''
|
||||
}`}
|
||||
style={{ boxShadow: '0 0 15px rgba(8,145,178,0.06), inset 0 0 20px rgba(0,0,0,0.4)', ...(expanded ? { flex: '1 1 0', minHeight: 0 } : {}) }}
|
||||
>
|
||||
{/* HEADER */}
|
||||
@@ -438,8 +450,8 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
|
||||
{ key: 'meshtastic' as Tab, label: 'MESH', icon: <Radio size={10} />, badge: 0 },
|
||||
{
|
||||
key: 'dms' as Tab,
|
||||
label: 'AGENT SHELL',
|
||||
icon: <Terminal size={10} />,
|
||||
label: 'SHELL',
|
||||
icon: <SquareTerminal size={10} />,
|
||||
badge: 0,
|
||||
},
|
||||
].map((tab) => (
|
||||
@@ -480,21 +492,21 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab !== 'meshtastic' && wormholeEnabled && !wormholeReadyState && (
|
||||
{activeTab !== 'dms' && activeTab !== 'meshtastic' && wormholeEnabled && !wormholeReadyState && (
|
||||
<div className="px-3 py-2 text-sm font-mono text-red-400/90 border-b border-red-900/30 bg-red-950/20 leading-[1.65] shrink-0">
|
||||
Wormhole secure mode is enabled but the local agent is not ready. Dead Drop is
|
||||
blocked until Wormhole is running.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab !== 'meshtastic' && wormholeEnabled && wormholeReadyState && (
|
||||
{activeTab !== 'dms' && activeTab !== 'meshtastic' && wormholeEnabled && wormholeReadyState && (
|
||||
<div className="px-3 py-2 text-sm font-mono text-yellow-400/80 border-b border-yellow-900/20 bg-yellow-950/10 leading-[1.65] shrink-0">
|
||||
Wormhole secure mode is active. Experimental private-lane operations are routed
|
||||
through the local agent and current secure transport paths.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab !== 'meshtastic' && wormholeEnabled && wormholeReadyState && !wormholeRnsReady && (
|
||||
{activeTab !== 'dms' && activeTab !== 'meshtastic' && wormholeEnabled && wormholeReadyState && !wormholeRnsReady && (
|
||||
<div className="px-3 py-2 text-sm font-mono text-amber-300/90 border-b border-amber-900/30 bg-amber-950/20 leading-[1.65] shrink-0">
|
||||
TRANSITIONAL PRIVATE LANE. Wormhole is up and gate chat is available on the
|
||||
transitional lane. Reticulum is still warming — Dead Drop / DM requires the
|
||||
@@ -502,7 +514,7 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab !== 'meshtastic' && anonymousModeEnabled && !anonymousModeReady && (
|
||||
{activeTab !== 'dms' && activeTab !== 'meshtastic' && anonymousModeEnabled && !anonymousModeReady && (
|
||||
<div className="px-3 py-2 text-sm font-mono text-red-400/90 border-b border-red-900/30 bg-red-950/20 leading-[1.65] shrink-0">
|
||||
Anonymous mode is active, but hidden transport is not ready. Dead Drop is blocked
|
||||
until Wormhole is running over Tor, I2P, or Mixnet.
|
||||
@@ -545,8 +557,9 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
|
||||
<div className="flex-1 overflow-hidden flex flex-col min-h-0">
|
||||
{activeTab === 'dms' && (
|
||||
<AgentShellPanel
|
||||
anchorRef={panelBoxRef}
|
||||
active={expanded && activeTab === 'dms'}
|
||||
expanded={shellExpanded}
|
||||
onExpandedChange={setShellExpanded}
|
||||
/>
|
||||
)}
|
||||
{dashboardRestrictedTab && (
|
||||
@@ -1502,7 +1515,7 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
|
||||
SHELL
|
||||
</span>
|
||||
<div className="px-3 py-2.5 text-[13px] font-mono text-[var(--text-secondary)] leading-relaxed">
|
||||
Local bash/cmd launches here (desktop). Set your own working directory in Settings.
|
||||
Real local shell over PTY. EXPAND widens this panel; SNAP docks it back into Mesh Chat.
|
||||
</div>
|
||||
</div>
|
||||
) : dashboardRestrictedTab ? (
|
||||
|
||||
Reference in New Issue
Block a user