@cryptiklemur/lattice 1.39.0 → 1.40.0
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/mesh/connector.ts +28 -12
- 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.0",
|
|
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) {
|
|
@@ -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") {
|
|
@@ -358,21 +362,33 @@ export function getConnectedPeerProjects(nodeId: string): Array<{ slug: string;
|
|
|
358
362
|
return conn.projects;
|
|
359
363
|
}
|
|
360
364
|
|
|
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
|
-
|
|
365
|
+
export function getAllRemoteProjects(localNodeId: string): Array<{ slug: string; path: string; title: string; nodeId: string; nodeName: string; isRemote: boolean; online: boolean }> {
|
|
366
|
+
var peersModule = require("./peers") as typeof import("./peers");
|
|
367
|
+
var allPeers = peersModule.loadPeers();
|
|
368
|
+
var connectedIds = new Set(getConnectedPeerIds());
|
|
369
|
+
var results: Array<{ slug: string; path: string; title: string; nodeId: string; nodeName: string; isRemote: boolean; online: boolean }> = [];
|
|
370
|
+
|
|
371
|
+
for (var p = 0; p < allPeers.length; p++) {
|
|
372
|
+
var peer = allPeers[p];
|
|
373
|
+
var isOnline = connectedIds.has(peer.id);
|
|
374
|
+
var projects: Array<{ slug: string; title: string }> = [];
|
|
375
|
+
|
|
376
|
+
var conn = connections.get(peer.id);
|
|
377
|
+
if (conn && conn.projects.length > 0) {
|
|
378
|
+
projects = conn.projects;
|
|
379
|
+
} else if (lastKnownProjects.has(peer.id)) {
|
|
380
|
+
projects = lastKnownProjects.get(peer.id)!;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
for (var i = 0; i < projects.length; i++) {
|
|
369
384
|
results.push({
|
|
370
|
-
slug:
|
|
385
|
+
slug: projects[i].slug,
|
|
371
386
|
path: "",
|
|
372
|
-
title:
|
|
373
|
-
nodeId:
|
|
374
|
-
nodeName:
|
|
387
|
+
title: projects[i].title,
|
|
388
|
+
nodeId: peer.id,
|
|
389
|
+
nodeName: peer.name,
|
|
375
390
|
isRemote: true,
|
|
391
|
+
online: isOnline,
|
|
376
392
|
});
|
|
377
393
|
}
|
|
378
394
|
}
|