@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 { useFocusTrap } from "../../hooks/useFocusTrap";
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 stableOnClose = useCallback(function () { props.onClose(); }, [props.onClose]);
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
- if (pairStatus !== "connecting") {
42
- return;
47
+ function handleInvite(msg: ServerMessage) {
48
+ if (msg.type === "mesh:invite_code") {
49
+ setGenerating(false);
50
+ }
43
51
  }
44
52
 
45
- function handler(msg: ServerMessage) {
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:paired", handler);
60
+ ws.subscribe("mesh:invite_code", handleInvite);
61
+ ws.subscribe("mesh:paired", handlePaired);
53
62
  return function () {
54
- ws.unsubscribe("mesh:paired", handler);
63
+ ws.unsubscribe("mesh:invite_code", handleInvite);
64
+ ws.unsubscribe("mesh:paired", handlePaired);
55
65
  };
56
- }, [ws, pairStatus]);
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&apos;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.1",
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
- generateInviteCode("localhost", config.port).then(function (result) {
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
- console.warn("[lattice] mesh:pair invalid invite code");
78
+ sendTo(clientId, { type: "chat:error", message: "Invalid invite code format" });
62
79
  return;
63
80
  }
64
81
  if (!validatePairingToken(parsed.token)) {
65
- console.warn("[lattice] mesh:pair invalid or expired token");
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 qrDataUrl = await QRCode.toString(code, { type: "svg" });
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
  }