Add MCP server (v1 read-only): toggle, per-pane gate, panel UI
This commit is contained in:
parent
6068522ee3
commit
83d8932c98
15 changed files with 1235 additions and 7 deletions
123
src/App.tsx
123
src/App.tsx
|
|
@ -7,10 +7,18 @@ import {
|
|||
saveSshHosts,
|
||||
setHostPassword,
|
||||
deleteHostPassword,
|
||||
mcpStart,
|
||||
mcpStop,
|
||||
mcpStatus as mcpStatusCmd,
|
||||
mcpUpdateState,
|
||||
writeToPane,
|
||||
killPane,
|
||||
type PaneId,
|
||||
type SshHost,
|
||||
type McpStatus,
|
||||
type McpMirror,
|
||||
type McpMirroredLeaf,
|
||||
type McpMirroredHost,
|
||||
} from "./ipc";
|
||||
import {
|
||||
type TreeNode,
|
||||
|
|
@ -27,6 +35,7 @@ import {
|
|||
setLeafShell,
|
||||
changeLabel,
|
||||
toggleBroadcast as toggleBroadcastInTree,
|
||||
toggleMcpAllow as toggleMcpAllowInTree,
|
||||
setAllBroadcast,
|
||||
adjustFontSize,
|
||||
adjustAllFontSizes,
|
||||
|
|
@ -53,6 +62,7 @@ import Notifications, { type Toast } from "./components/Notifications";
|
|||
import Palette from "./components/Palette";
|
||||
import HostManager from "./components/HostManager";
|
||||
import Help from "./components/Help";
|
||||
import McpPanel from "./components/McpPanel";
|
||||
import "./App.css";
|
||||
import "./lib/layout/Gutter.css";
|
||||
|
||||
|
|
@ -86,6 +96,12 @@ export default function App() {
|
|||
const [hosts, setHosts] = useState<SshHost[]>([]);
|
||||
const [hostManagerOpen, setHostManagerOpen] = useState(false);
|
||||
const [helpOpen, setHelpOpen] = useState(false);
|
||||
const [mcpStatus, setMcpStatus] = useState<McpStatus>({
|
||||
running: false,
|
||||
url: null,
|
||||
token: null,
|
||||
});
|
||||
const [mcpPanelOpen, setMcpPanelOpen] = useState(false);
|
||||
const [ready, setReady] = useState(false);
|
||||
const [notifications, setNotifications] = useState<Toast[]>([]);
|
||||
const [paletteOpen, setPaletteOpen] = useState(false);
|
||||
|
|
@ -261,6 +277,46 @@ export default function App() {
|
|||
setTree((t) => toggleBroadcastInTree(t, leafId));
|
||||
}, []);
|
||||
|
||||
const toggleMcpAllow = useCallback((leafId: NodeId) => {
|
||||
setTree((t) => toggleMcpAllowInTree(t, leafId));
|
||||
}, []);
|
||||
|
||||
// ---- MCP server lifecycle ------------------------------------------------
|
||||
const refreshMcpStatus = useCallback(async () => {
|
||||
try {
|
||||
const st = await mcpStatusCmd();
|
||||
setMcpStatus(st);
|
||||
} catch (e) {
|
||||
console.warn("mcpStatus failed:", e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const startMcp = useCallback(async () => {
|
||||
try {
|
||||
const st = await mcpStart();
|
||||
setMcpStatus(st);
|
||||
notify("MCP server started — see panel for URL + token");
|
||||
} catch (e) {
|
||||
notify(`MCP start failed: ${e}`);
|
||||
}
|
||||
}, [notify]);
|
||||
|
||||
const stopMcp = useCallback(async () => {
|
||||
try {
|
||||
const st = await mcpStop();
|
||||
setMcpStatus(st);
|
||||
notify("MCP server stopped");
|
||||
} catch (e) {
|
||||
notify(`MCP stop failed: ${e}`);
|
||||
}
|
||||
}, [notify]);
|
||||
|
||||
// On mount, sync our local mcpStatus with whatever's already running
|
||||
// (the backend persists state across HMR reloads).
|
||||
useEffect(() => {
|
||||
void refreshMcpStatus();
|
||||
}, [refreshMcpStatus]);
|
||||
|
||||
// Ctrl+Shift+P: pop the active leaf out one level. The keyboard
|
||||
// replacement for the (removed) drag-past-sibling gesture. No-op with a
|
||||
// toast if the leaf is at the root or its parent shares orientation
|
||||
|
|
@ -532,6 +588,7 @@ export default function App() {
|
|||
setShell,
|
||||
setLabel,
|
||||
toggleBroadcast,
|
||||
toggleMcpAllow,
|
||||
openHostManager,
|
||||
setActive,
|
||||
registerPaneId,
|
||||
|
|
@ -553,6 +610,7 @@ export default function App() {
|
|||
setShell,
|
||||
setLabel,
|
||||
toggleBroadcast,
|
||||
toggleMcpAllow,
|
||||
openHostManager,
|
||||
setActive,
|
||||
registerPaneId,
|
||||
|
|
@ -567,6 +625,47 @@ export default function App() {
|
|||
],
|
||||
);
|
||||
|
||||
// ---- MCP mirror push -----------------------------------------------------
|
||||
// Whenever the tree, hosts, or active selection change AND the MCP server
|
||||
// is running, push a fresh mirror down to the backend. Per-leaf mcpAllow
|
||||
// gates whether each leaf appears in the mirror (default-deny).
|
||||
const allowedPaneCount = useMemo(
|
||||
() => Array.from(walkLeaves(tree)).filter((l) => l.mcpAllow).length,
|
||||
[tree],
|
||||
);
|
||||
useEffect(() => {
|
||||
if (!mcpStatus.running) return;
|
||||
const leaves: Record<string, McpMirroredLeaf> = {};
|
||||
for (const leaf of walkLeaves(tree)) {
|
||||
if (!leaf.mcpAllow) continue;
|
||||
leaves[leaf.id] = {
|
||||
paneId: paneIdByLeafRef.current.get(leaf.id) ?? null,
|
||||
label: leaf.label,
|
||||
shellKind: leaf.shellKind,
|
||||
distro: leaf.distro,
|
||||
sshHostId: leaf.sshHostId,
|
||||
broadcast: !!leaf.broadcast,
|
||||
active: activeLeafId === leaf.id,
|
||||
};
|
||||
}
|
||||
const mirroredHosts: McpMirroredHost[] = hosts.map((h) => ({
|
||||
id: h.id,
|
||||
label: h.label,
|
||||
hostname: h.hostname,
|
||||
user: h.user,
|
||||
port: h.port,
|
||||
hasPassword: !!h.hasPassword,
|
||||
}));
|
||||
const mirror: McpMirror = {
|
||||
layoutJson: serialize(tree),
|
||||
leaves,
|
||||
hosts: mirroredHosts,
|
||||
};
|
||||
mcpUpdateState(mirror).catch((e) =>
|
||||
console.warn("mcpUpdateState failed:", e),
|
||||
);
|
||||
}, [mcpStatus.running, tree, hosts, activeLeafId]);
|
||||
|
||||
const applyPreset = useCallback(
|
||||
(make: (d: Partial<LeafNode>) => TreeNode) => {
|
||||
const { tree: nextTree, dropped } = reshapeToPreset(
|
||||
|
|
@ -723,6 +822,19 @@ export default function App() {
|
|||
>
|
||||
🔔
|
||||
</button>
|
||||
<button
|
||||
className={`palette-btn mcp-btn${mcpStatus.running ? " on" : ""}`}
|
||||
onClick={() => setMcpPanelOpen(true)}
|
||||
title={
|
||||
mcpStatus.running
|
||||
? `MCP server running (${allowedPaneCount} of ${leafCount(tree)} panes visible) — click for details`
|
||||
: "MCP server is OFF — click to configure / start"
|
||||
}
|
||||
aria-label="MCP server"
|
||||
aria-pressed={mcpStatus.running ? "true" : "false"}
|
||||
>
|
||||
🤖
|
||||
</button>
|
||||
<button
|
||||
className="palette-btn"
|
||||
onClick={() => setHelpOpen(true)}
|
||||
|
|
@ -794,6 +906,17 @@ export default function App() {
|
|||
)}
|
||||
|
||||
{helpOpen && <Help onClose={() => setHelpOpen(false)} />}
|
||||
|
||||
{mcpPanelOpen && (
|
||||
<McpPanel
|
||||
status={mcpStatus}
|
||||
onStart={startMcp}
|
||||
onStop={stopMcp}
|
||||
onClose={() => setMcpPanelOpen(false)}
|
||||
allowedPaneCount={allowedPaneCount}
|
||||
totalPaneCount={leafCount(tree)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue