@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.
@@ -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 : false,
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 list = (msg as ProjectsListMessage).projects;
26
- setProjects(list);
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 = list.find(function (p: typeof list[number]) { return p.slug === currentSlug; });
31
- if (!found && list.length > 0) {
32
- setActiveProjectSlug(list[0].slug);
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
  }
@@ -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.39.0",
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>",
@@ -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 results: Array<{ slug: string; path: string; title: string; nodeId: string; nodeName: string; isRemote: boolean }> = [];
363
- for (var [nodeId, conn] of connections) {
364
- if (conn.ws.readyState !== WebSocket.OPEN) continue;
365
- var peers = require("./peers") as typeof import("./peers");
366
- var peer = peers.getPeer(nodeId);
367
- var peerName = peer ? peer.name : nodeId;
368
- for (var i = 0; i < conn.projects.length; i++) {
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: conn.projects[i].slug,
385
+ slug: projects[i].slug,
371
386
  path: "",
372
- title: conn.projects[i].title,
373
- nodeId: nodeId,
374
- nodeName: peerName,
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
  }
@@ -21,6 +21,7 @@ export interface ProjectSummary {
21
21
  export interface ProjectInfo extends ProjectSummary {
22
22
  nodeName: string;
23
23
  isRemote: boolean;
24
+ online?: boolean;
24
25
  ideProjectName?: string;
25
26
  }
26
27