@cryptiklemur/lattice 1.28.1 → 1.28.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback, useRef } from "react";
|
|
2
|
-
import {
|
|
3
|
-
import { X, Copy, Check } from "lucide-react";
|
|
2
|
+
import { X, Copy, Check, Loader2 } from "lucide-react";
|
|
4
3
|
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
5
4
|
import { useMesh } from "../../hooks/useMesh";
|
|
6
5
|
import { clearInvite } from "../../stores/mesh";
|
|
@@ -22,9 +21,9 @@ export function PairingDialog(props: PairingDialogProps) {
|
|
|
22
21
|
var [pairStatus, setPairStatus] = useState<PairStatus>("idle");
|
|
23
22
|
var [pairError, setPairError] = useState<string | null>(null);
|
|
24
23
|
var [copied, setCopied] = useState(false);
|
|
24
|
+
var [generating, setGenerating] = useState(false);
|
|
25
25
|
var modalRef = useRef<HTMLDivElement>(null);
|
|
26
|
-
var
|
|
27
|
-
useFocusTrap(modalRef, stableOnClose, props.isOpen);
|
|
26
|
+
var inputRef = useRef<HTMLInputElement>(null);
|
|
28
27
|
|
|
29
28
|
useEffect(function () {
|
|
30
29
|
if (!props.isOpen) {
|
|
@@ -33,30 +32,42 @@ export function PairingDialog(props: PairingDialogProps) {
|
|
|
33
32
|
setPairStatus("idle");
|
|
34
33
|
setPairError(null);
|
|
35
34
|
setCopied(false);
|
|
35
|
+
setGenerating(false);
|
|
36
36
|
setTab("generate");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
40
|
+
if (e.key === "Escape") props.onClose();
|
|
37
41
|
}
|
|
42
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
43
|
+
return function () { document.removeEventListener("keydown", handleKeyDown); };
|
|
38
44
|
}, [props.isOpen]);
|
|
39
45
|
|
|
40
46
|
useEffect(function () {
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
function handleInvite(msg: ServerMessage) {
|
|
48
|
+
if (msg.type === "mesh:invite_code") {
|
|
49
|
+
setGenerating(false);
|
|
50
|
+
}
|
|
43
51
|
}
|
|
44
52
|
|
|
45
|
-
function
|
|
53
|
+
function handlePaired(msg: ServerMessage) {
|
|
46
54
|
if (msg.type === "mesh:paired") {
|
|
47
55
|
setPairStatus("paired");
|
|
48
56
|
setPairError(null);
|
|
49
57
|
}
|
|
50
58
|
}
|
|
51
59
|
|
|
52
|
-
ws.subscribe("mesh:
|
|
60
|
+
ws.subscribe("mesh:invite_code", handleInvite);
|
|
61
|
+
ws.subscribe("mesh:paired", handlePaired);
|
|
53
62
|
return function () {
|
|
54
|
-
ws.unsubscribe("mesh:
|
|
63
|
+
ws.unsubscribe("mesh:invite_code", handleInvite);
|
|
64
|
+
ws.unsubscribe("mesh:paired", handlePaired);
|
|
55
65
|
};
|
|
56
|
-
}, [
|
|
66
|
+
}, []);
|
|
57
67
|
|
|
58
68
|
function handleGenerateInvite() {
|
|
59
69
|
clearInvite();
|
|
70
|
+
setGenerating(true);
|
|
60
71
|
mesh.generateInvite();
|
|
61
72
|
}
|
|
62
73
|
|
|
@@ -152,7 +163,7 @@ export function PairingDialog(props: PairingDialogProps) {
|
|
|
152
163
|
The code encodes this node's address and a one-time auth token.
|
|
153
164
|
</div>
|
|
154
165
|
|
|
155
|
-
{!mesh.inviteCode && (
|
|
166
|
+
{!mesh.inviteCode && !generating && (
|
|
156
167
|
<button
|
|
157
168
|
onClick={handleGenerateInvite}
|
|
158
169
|
className="btn btn-primary btn-sm"
|
|
@@ -161,6 +172,13 @@ export function PairingDialog(props: PairingDialogProps) {
|
|
|
161
172
|
</button>
|
|
162
173
|
)}
|
|
163
174
|
|
|
175
|
+
{generating && !mesh.inviteCode && (
|
|
176
|
+
<div className="flex items-center gap-2 text-[13px] text-base-content/40">
|
|
177
|
+
<Loader2 size={14} className="animate-spin text-primary" />
|
|
178
|
+
Generating invite code...
|
|
179
|
+
</div>
|
|
180
|
+
)}
|
|
181
|
+
|
|
164
182
|
{mesh.inviteCode && (
|
|
165
183
|
<div>
|
|
166
184
|
<div className="flex items-center gap-2 px-3.5 py-2.5 rounded bg-base-100 border border-base-300 mb-4">
|
|
@@ -209,6 +227,7 @@ export function PairingDialog(props: PairingDialogProps) {
|
|
|
209
227
|
</div>
|
|
210
228
|
|
|
211
229
|
<input
|
|
230
|
+
ref={inputRef}
|
|
212
231
|
type="text"
|
|
213
232
|
value={pairCode}
|
|
214
233
|
onChange={function (e) {
|
|
@@ -224,6 +243,7 @@ export function PairingDialog(props: PairingDialogProps) {
|
|
|
224
243
|
}
|
|
225
244
|
}}
|
|
226
245
|
placeholder="LTCE-XXXX-XXXX"
|
|
246
|
+
autoFocus
|
|
227
247
|
disabled={pairStatus === "connecting" || pairStatus === "paired"}
|
|
228
248
|
className="input input-bordered w-full bg-base-100 text-base-content font-mono text-[14px] tracking-[0.06em] mb-3 focus:border-primary"
|
|
229
249
|
/>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cryptiklemur/lattice",
|
|
3
|
-
"version": "1.28.
|
|
3
|
+
"version": "1.28.2",
|
|
4
4
|
"description": "Multi-machine agentic dashboard for Claude Code. Monitor sessions, manage MCP servers and skills, orchestrate across mesh-networked nodes.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Aaron Scherer <me@aaronscherer.me>",
|
|
@@ -4,6 +4,22 @@ import { sendTo, broadcast } from "../ws/broadcast";
|
|
|
4
4
|
import { loadConfig } from "../config";
|
|
5
5
|
import { loadOrCreateIdentity } from "../identity";
|
|
6
6
|
import { generateInviteCode, parseInviteCode, validatePairingToken, consumePairingToken } from "../mesh/pairing";
|
|
7
|
+
import { networkInterfaces } from "node:os";
|
|
8
|
+
|
|
9
|
+
function getLocalAddress(): string {
|
|
10
|
+
var interfaces = networkInterfaces();
|
|
11
|
+
var keys = Object.keys(interfaces);
|
|
12
|
+
for (var i = 0; i < keys.length; i++) {
|
|
13
|
+
var addrs = interfaces[keys[i]];
|
|
14
|
+
if (!addrs) continue;
|
|
15
|
+
for (var j = 0; j < addrs.length; j++) {
|
|
16
|
+
if (!addrs[j].internal && addrs[j].family === "IPv4") {
|
|
17
|
+
return addrs[j].address;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return "localhost";
|
|
22
|
+
}
|
|
7
23
|
import { addPeer, removePeer, loadPeers } from "../mesh/peers";
|
|
8
24
|
import type { PeerInfo } from "@lattice/shared";
|
|
9
25
|
|
|
@@ -42,7 +58,8 @@ export function buildNodesMessage(): NodeInfo[] {
|
|
|
42
58
|
registerHandler("mesh", function (clientId: string, message: ClientMessage) {
|
|
43
59
|
if (message.type === "mesh:generate_invite") {
|
|
44
60
|
var config = loadConfig();
|
|
45
|
-
|
|
61
|
+
var address = getLocalAddress();
|
|
62
|
+
generateInviteCode(address, config.port).then(function (result) {
|
|
46
63
|
sendTo(clientId, {
|
|
47
64
|
type: "mesh:invite_code",
|
|
48
65
|
code: result.code,
|
|
@@ -58,11 +75,11 @@ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
|
|
|
58
75
|
var pairMsg = message as MeshPairMessage;
|
|
59
76
|
var parsed = parseInviteCode(pairMsg.code);
|
|
60
77
|
if (!parsed) {
|
|
61
|
-
|
|
78
|
+
sendTo(clientId, { type: "chat:error", message: "Invalid invite code format" });
|
|
62
79
|
return;
|
|
63
80
|
}
|
|
64
81
|
if (!validatePairingToken(parsed.token)) {
|
|
65
|
-
|
|
82
|
+
sendTo(clientId, { type: "chat:error", message: "Invite code is invalid or expired" });
|
|
66
83
|
return;
|
|
67
84
|
}
|
|
68
85
|
consumePairingToken(parsed.token);
|
|
@@ -110,6 +127,7 @@ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
|
|
|
110
127
|
});
|
|
111
128
|
ws.addEventListener("error", function () {
|
|
112
129
|
console.error("[lattice] mesh:pair — failed to connect to", wsUrl);
|
|
130
|
+
sendTo(clientId, { type: "chat:error", message: "Failed to connect to " + parsed!.address + ":" + parsed!.port });
|
|
113
131
|
});
|
|
114
132
|
return;
|
|
115
133
|
}
|
|
@@ -60,7 +60,8 @@ export async function generateInviteCode(
|
|
|
60
60
|
|
|
61
61
|
pendingTokens.set(token, Date.now());
|
|
62
62
|
|
|
63
|
-
var
|
|
63
|
+
var qrSvg = await QRCode.toString(code, { type: "svg" });
|
|
64
|
+
var qrDataUrl = "data:image/svg+xml;base64," + Buffer.from(qrSvg).toString("base64");
|
|
64
65
|
|
|
65
66
|
return { code, token, qrDataUrl };
|
|
66
67
|
}
|