@cryptiklemur/lattice 1.39.0 → 1.40.1
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.
- package/client/src/components/sidebar/ProjectRail.tsx +1 -1
- package/client/src/components/ui/NodeDisconnectedOverlay.tsx +35 -0
- package/client/src/hooks/useProjects.ts +21 -5
- package/client/src/router.tsx +3 -1
- package/package.json +1 -1
- package/server/src/daemon.ts +1 -1
- package/server/src/handlers/mesh.ts +2 -2
- package/server/src/mesh/connector.ts +35 -14
- package/shared/src/models.ts +1 -0
|
@@ -31,7 +31,7 @@ function groupProjectsBySlug(projects: ProjectInfo[], nodes: NodeInfo[]): Projec
|
|
|
31
31
|
var nodeEntry = {
|
|
32
32
|
nodeId: p.nodeId,
|
|
33
33
|
nodeName: p.nodeName,
|
|
34
|
-
online: node ? node.online :
|
|
34
|
+
online: node ? node.online : (p.online ?? !p.isRemote),
|
|
35
35
|
path: p.path,
|
|
36
36
|
};
|
|
37
37
|
if (existing) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { WifiOff, Loader2 } from "lucide-react";
|
|
2
|
+
import { useProjects } from "../../hooks/useProjects";
|
|
3
|
+
import { useMesh } from "../../hooks/useMesh";
|
|
4
|
+
import { useSidebar } from "../../hooks/useSidebar";
|
|
5
|
+
|
|
6
|
+
export function NodeDisconnectedOverlay() {
|
|
7
|
+
var { activeProject } = useProjects();
|
|
8
|
+
var { nodes } = useMesh();
|
|
9
|
+
var sidebar = useSidebar();
|
|
10
|
+
|
|
11
|
+
if (!activeProject || !activeProject.isRemote) return null;
|
|
12
|
+
if (sidebar.activeView.type !== "chat") return null;
|
|
13
|
+
|
|
14
|
+
var remoteNode = nodes.find(function (n) { return n.id === activeProject!.nodeId; });
|
|
15
|
+
if (!remoteNode || remoteNode.online) return null;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="absolute inset-0 z-40 flex items-center justify-center bg-base-100/80 backdrop-blur-sm">
|
|
19
|
+
<div className="bg-base-300 border border-base-content/15 rounded-2xl shadow-2xl px-8 py-6 max-w-sm text-center">
|
|
20
|
+
<WifiOff size={28} className="text-warning mx-auto mb-3" />
|
|
21
|
+
<h3 className="text-[15px] font-mono font-bold text-base-content mb-1">Node Disconnected</h3>
|
|
22
|
+
<p className="text-[13px] text-base-content/50 mb-3">
|
|
23
|
+
<span className="font-semibold text-base-content/70">{remoteNode.name}</span> is unreachable.
|
|
24
|
+
</p>
|
|
25
|
+
<div className="flex items-center justify-center gap-2 text-[12px] text-base-content/40">
|
|
26
|
+
<Loader2 size={12} className="animate-spin" />
|
|
27
|
+
Attempting to reconnect...
|
|
28
|
+
</div>
|
|
29
|
+
<p className="text-[11px] text-base-content/25 mt-3">
|
|
30
|
+
Session state preserved. Operations will resume on reconnect.
|
|
31
|
+
</p>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -22,14 +22,30 @@ export function useProjects(): UseProjectsResult {
|
|
|
22
22
|
useEffect(function () {
|
|
23
23
|
handleRef.current = function (msg: ServerMessage) {
|
|
24
24
|
if (msg.type === "projects:list") {
|
|
25
|
-
var
|
|
26
|
-
setProjects(
|
|
25
|
+
var incoming = (msg as ProjectsListMessage).projects;
|
|
26
|
+
setProjects(function (prev) {
|
|
27
|
+
var incomingKeys = new Set(incoming.map(function (p) { return p.slug + "@" + p.nodeId; }));
|
|
28
|
+
var kept = prev.filter(function (p) {
|
|
29
|
+
if (!p.isRemote) return false;
|
|
30
|
+
return !incomingKeys.has(p.slug + "@" + p.nodeId);
|
|
31
|
+
});
|
|
32
|
+
for (var i = 0; i < kept.length; i++) {
|
|
33
|
+
(kept[i] as any).online = false;
|
|
34
|
+
}
|
|
35
|
+
return incoming.concat(kept);
|
|
36
|
+
});
|
|
27
37
|
var storeState = getSidebarStore().state;
|
|
28
38
|
var currentSlug = storeState.activeProjectSlug;
|
|
29
39
|
if (currentSlug !== null) {
|
|
30
|
-
var found =
|
|
31
|
-
if (!found
|
|
32
|
-
|
|
40
|
+
var found = incoming.find(function (p: typeof incoming[number]) { return p.slug === currentSlug; });
|
|
41
|
+
if (!found) {
|
|
42
|
+
setProjects(function (current) {
|
|
43
|
+
var stillExists = current.find(function (p) { return p.slug === currentSlug; });
|
|
44
|
+
if (!stillExists && current.length > 0) {
|
|
45
|
+
setActiveProjectSlug(current[0].slug);
|
|
46
|
+
}
|
|
47
|
+
return current;
|
|
48
|
+
});
|
|
33
49
|
}
|
|
34
50
|
}
|
|
35
51
|
}
|
package/client/src/router.tsx
CHANGED
|
@@ -16,6 +16,7 @@ import { useSidebar } from "./hooks/useSidebar";
|
|
|
16
16
|
import { useWorkspace } from "./hooks/useWorkspace";
|
|
17
17
|
import { useWebSocket } from "./hooks/useWebSocket";
|
|
18
18
|
import { UpdateBanner } from "./components/ui/UpdateBanner";
|
|
19
|
+
import { NodeDisconnectedOverlay } from "./components/ui/NodeDisconnectedOverlay";
|
|
19
20
|
import { useSwipeDrawer } from "./hooks/useSwipeDrawer";
|
|
20
21
|
import { exitSettings, getSidebarStore, handlePopState, closeDrawer, toggleDrawer } from "./stores/sidebar";
|
|
21
22
|
|
|
@@ -422,9 +423,10 @@ function RootLayout() {
|
|
|
422
423
|
readOnly
|
|
423
424
|
/>
|
|
424
425
|
|
|
425
|
-
<main id="main-content" className="drawer-content flex flex-col h-full min-w-0 overflow-hidden">
|
|
426
|
+
<main id="main-content" className="drawer-content flex flex-col h-full min-w-0 overflow-hidden relative">
|
|
426
427
|
<UpdateBanner />
|
|
427
428
|
<Outlet />
|
|
429
|
+
<NodeDisconnectedOverlay />
|
|
428
430
|
</main>
|
|
429
431
|
|
|
430
432
|
<div ref={drawerSideRef} className="drawer-side z-50 h-full">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cryptiklemur/lattice",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.40.1",
|
|
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>",
|
package/server/src/daemon.ts
CHANGED
|
@@ -401,7 +401,7 @@ export async function startDaemon(portOverride?: number | null): Promise<void> {
|
|
|
401
401
|
var remoteProjects = getAllRemoteProjects(currentIdentity.id);
|
|
402
402
|
broadcast({
|
|
403
403
|
type: "projects:list",
|
|
404
|
-
projects: localProjects.concat(remoteProjects as typeof localProjects),
|
|
404
|
+
projects: localProjects.concat(remoteProjects as unknown as typeof localProjects),
|
|
405
405
|
});
|
|
406
406
|
var updateInfo = getCachedUpdateInfo();
|
|
407
407
|
if (updateInfo && updateInfo.updateAvailable) {
|
|
@@ -245,9 +245,9 @@ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
|
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
var inboundWs = getClientWebSocket(clientId);
|
|
248
|
-
log.meshHello(" registering inbound connection for %s (ws=%s)", hello.name, !!inboundWs);
|
|
248
|
+
log.meshHello(" registering inbound connection for %s (ws=%s, projects=%d)", hello.name, !!inboundWs, hello.projects?.length ?? 0);
|
|
249
249
|
if (inboundWs) {
|
|
250
|
-
registerInboundPeer(hello.nodeId, inboundWs as any);
|
|
250
|
+
registerInboundPeer(hello.nodeId, inboundWs as any, hello.projects ?? []);
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
var identity = loadOrCreateIdentity();
|
|
@@ -17,6 +17,7 @@ interface PeerConnection {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
var connections = new Map<string, PeerConnection>();
|
|
20
|
+
var lastKnownProjects = new Map<string, Array<{ slug: string; title: string }>>();
|
|
20
21
|
var connectedCallbacks: Array<(nodeId: string) => void> = [];
|
|
21
22
|
var disconnectedCallbacks: Array<(nodeId: string) => void> = [];
|
|
22
23
|
var messageCallbacks: Array<(nodeId: string, msg: MeshMessage) => void> = [];
|
|
@@ -176,6 +177,9 @@ function openConnection(conn: PeerConnection, url: string): void {
|
|
|
176
177
|
|
|
177
178
|
if (msg.type === "mesh:hello") {
|
|
178
179
|
conn.projects = msg.projects;
|
|
180
|
+
if (msg.projects.length > 0) {
|
|
181
|
+
lastKnownProjects.set(conn.nodeId, msg.projects);
|
|
182
|
+
}
|
|
179
183
|
} else if (msg.type === "mesh:session_sync") {
|
|
180
184
|
handleSessionSync(conn.nodeId, msg as MeshSessionSyncMessage);
|
|
181
185
|
} else if (msg.type === "mesh:session_request") {
|
|
@@ -248,7 +252,7 @@ export function getPeerConnection(nodeId: string): WebSocket | undefined {
|
|
|
248
252
|
return conn.ws;
|
|
249
253
|
}
|
|
250
254
|
|
|
251
|
-
export function registerInboundPeer(nodeId: string, ws: { send: (data: string) => void; readyState: number }): void {
|
|
255
|
+
export function registerInboundPeer(nodeId: string, ws: { send: (data: string) => void; readyState: number }, peerProjects?: Array<{ slug: string; title: string }>): void {
|
|
252
256
|
var existing = connections.get(nodeId);
|
|
253
257
|
if (existing && !existing.dead && existing.ws.readyState === WebSocket.OPEN) {
|
|
254
258
|
log.meshConnect("inbound peer %s already connected, skipping", nodeId.slice(0, 8));
|
|
@@ -265,15 +269,20 @@ export function registerInboundPeer(nodeId: string, ws: { send: (data: string) =
|
|
|
265
269
|
|
|
266
270
|
circuitBreakers.delete(nodeId);
|
|
267
271
|
|
|
272
|
+
var incomingProjects = peerProjects ?? [];
|
|
268
273
|
var conn: PeerConnection = {
|
|
269
274
|
nodeId: nodeId,
|
|
270
275
|
ws: ws as WebSocket,
|
|
271
276
|
backoffMs: 1000,
|
|
272
277
|
retryTimer: null,
|
|
273
278
|
dead: false,
|
|
274
|
-
projects:
|
|
279
|
+
projects: incomingProjects,
|
|
275
280
|
};
|
|
276
281
|
|
|
282
|
+
if (incomingProjects.length > 0) {
|
|
283
|
+
lastKnownProjects.set(nodeId, incomingProjects);
|
|
284
|
+
}
|
|
285
|
+
|
|
277
286
|
connections.set(nodeId, conn);
|
|
278
287
|
|
|
279
288
|
var peers = loadPeers();
|
|
@@ -358,21 +367,33 @@ export function getConnectedPeerProjects(nodeId: string): Array<{ slug: string;
|
|
|
358
367
|
return conn.projects;
|
|
359
368
|
}
|
|
360
369
|
|
|
361
|
-
export function getAllRemoteProjects(localNodeId: string): Array<{ slug: string; path: string; title: string; nodeId: string; nodeName: string; isRemote: boolean }> {
|
|
362
|
-
var
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
370
|
+
export function getAllRemoteProjects(localNodeId: string): Array<{ slug: string; path: string; title: string; nodeId: string; nodeName: string; isRemote: boolean; online: boolean }> {
|
|
371
|
+
var peersModule = require("./peers") as typeof import("./peers");
|
|
372
|
+
var allPeers = peersModule.loadPeers();
|
|
373
|
+
var connectedIds = new Set(getConnectedPeerIds());
|
|
374
|
+
var results: Array<{ slug: string; path: string; title: string; nodeId: string; nodeName: string; isRemote: boolean; online: boolean }> = [];
|
|
375
|
+
|
|
376
|
+
for (var p = 0; p < allPeers.length; p++) {
|
|
377
|
+
var peer = allPeers[p];
|
|
378
|
+
var isOnline = connectedIds.has(peer.id);
|
|
379
|
+
var projects: Array<{ slug: string; title: string }> = [];
|
|
380
|
+
|
|
381
|
+
var conn = connections.get(peer.id);
|
|
382
|
+
if (conn && conn.projects.length > 0) {
|
|
383
|
+
projects = conn.projects;
|
|
384
|
+
} else if (lastKnownProjects.has(peer.id)) {
|
|
385
|
+
projects = lastKnownProjects.get(peer.id)!;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
for (var i = 0; i < projects.length; i++) {
|
|
369
389
|
results.push({
|
|
370
|
-
slug:
|
|
390
|
+
slug: projects[i].slug,
|
|
371
391
|
path: "",
|
|
372
|
-
title:
|
|
373
|
-
nodeId:
|
|
374
|
-
nodeName:
|
|
392
|
+
title: projects[i].title,
|
|
393
|
+
nodeId: peer.id,
|
|
394
|
+
nodeName: peer.name,
|
|
375
395
|
isRemote: true,
|
|
396
|
+
online: isOnline,
|
|
376
397
|
});
|
|
377
398
|
}
|
|
378
399
|
}
|