@cryptiklemur/lattice 1.35.0 → 1.36.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.
@@ -111,7 +111,7 @@ function ProjectButton(props: ProjectButtonProps) {
111
111
 
112
112
  {hovered && (
113
113
  <div
114
- className="pointer-events-none z-[9000] bg-base-300 border border-base-content/20 rounded px-2 py-1 text-xs text-base-content whitespace-nowrap shadow-xl"
114
+ className="pointer-events-none z-[9000] bg-base-300 border border-base-content/20 rounded-lg px-2.5 py-1.5 shadow-xl"
115
115
  style={{
116
116
  position: "fixed",
117
117
  left: "calc(64px + 8px)",
@@ -119,7 +119,18 @@ function ProjectButton(props: ProjectButtonProps) {
119
119
  transform: "translateY(-50%)",
120
120
  }}
121
121
  >
122
- {props.group.title}
122
+ <div className="text-[12px] font-bold text-base-content whitespace-nowrap">{props.group.title}</div>
123
+ {props.group.nodes.map(function (n) {
124
+ return (
125
+ <div key={n.nodeId} className="flex items-center gap-1.5 mt-0.5">
126
+ <div className={"w-[6px] h-[6px] rounded-full flex-shrink-0 " + (n.online ? "bg-success" : "bg-error")} />
127
+ <span className="text-[10px] text-base-content/50 whitespace-nowrap">
128
+ {n.nodeName}
129
+ {n.path ? " \u00B7 " + n.path : ""}
130
+ </span>
131
+ </div>
132
+ );
133
+ })}
123
134
  </div>
124
135
  )}
125
136
  </div>
@@ -331,6 +331,11 @@ export function Sidebar({ onSessionSelect }: { onSessionSelect?: () => void }) {
331
331
  >
332
332
  <span className="text-[13px] font-mono font-bold text-base-content/90">
333
333
  {activeProject?.title ?? "No Project"}
334
+ {activeProject?.isRemote && (
335
+ <span className="ml-1.5 text-[10px] font-normal text-base-content/30">
336
+ on {activeProject.nodeName}
337
+ </span>
338
+ )}
334
339
  </span>
335
340
  <ChevronDown size={14} className="text-base-content/30" />
336
341
  </button>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cryptiklemur/lattice",
3
- "version": "1.35.0",
3
+ "version": "1.36.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>",
@@ -8,7 +8,7 @@ import { addClient, removeClient, routeMessage } from "./ws/server";
8
8
  import { broadcast, sendTo } from "./ws/broadcast";
9
9
  import { buildNodesMessage } from "./handlers/mesh";
10
10
  import { startDiscovery } from "./mesh/discovery";
11
- import { startMeshConnections, onPeerConnected, onPeerDisconnected, onPeerMessage } from "./mesh/connector";
11
+ import { startMeshConnections, onPeerConnected, onPeerDisconnected, onPeerMessage, getAllRemoteProjects } from "./mesh/connector";
12
12
  import { handleProxyRequest, handleProxyResponse } from "./mesh/proxy";
13
13
  import { verifyPassphrase, generateSessionToken, addSession, isValidSession } from "./auth/passphrase";
14
14
  import { ensureCerts } from "./tls";
@@ -395,11 +395,13 @@ export async function startDaemon(portOverride?: number | null): Promise<void> {
395
395
  var currentConfig = loadConfig();
396
396
  var currentIdentity = loadOrCreateIdentity();
397
397
  broadcast({ type: "mesh:nodes", nodes: buildNodesMessage() });
398
+ var localProjects = currentConfig.projects.map(function (p: typeof currentConfig.projects[number]) {
399
+ return { slug: p.slug, path: p.path, title: p.title, nodeId: currentIdentity.id, nodeName: currentConfig.name, isRemote: false, ideProjectName: detectIdeProjectName(p.path) };
400
+ });
401
+ var remoteProjects = getAllRemoteProjects(currentIdentity.id);
398
402
  broadcast({
399
403
  type: "projects:list",
400
- projects: currentConfig.projects.map(function (p: typeof currentConfig.projects[number]) {
401
- return { slug: p.slug, path: p.path, title: p.title, nodeId: currentIdentity.id, nodeName: currentConfig.name, isRemote: false, ideProjectName: detectIdeProjectName(p.path) };
402
- }),
404
+ projects: localProjects.concat(remoteProjects as typeof localProjects),
403
405
  });
404
406
  var updateInfo = getCachedUpdateInfo();
405
407
  if (updateInfo && updateInfo.updateAvailable) {
@@ -5,7 +5,7 @@ import { loadConfig } from "../config";
5
5
  import { loadOrCreateIdentity } from "../identity";
6
6
  import { generateInviteCode, parseInviteCode, validatePairingToken, consumePairingToken } from "../mesh/pairing";
7
7
  import { addPeer, removePeer, loadPeers, getPeer } from "../mesh/peers";
8
- import { getConnectedPeerIds, connectToPeer, reconnectPeer, getPeerConnection, disconnectPeer } from "../mesh/connector";
8
+ import { getConnectedPeerIds, connectToPeer, reconnectPeer, getPeerConnection, disconnectPeer, getConnectedPeerProjects } from "../mesh/connector";
9
9
  import type { PeerInfo } from "@lattice/shared";
10
10
  import { networkInterfaces } from "node:os";
11
11
  import { existsSync, readFileSync } from "node:fs";
@@ -74,6 +74,13 @@ function getWindowsHostAddresses(): Array<{ name: string; address: string }> {
74
74
  return results;
75
75
  }
76
76
 
77
+ function getLocalProjectsList(): Array<{ slug: string; title: string }> {
78
+ var config = loadConfig();
79
+ return config.projects.map(function (p: typeof config.projects[number]) {
80
+ return { slug: p.slug, title: p.title };
81
+ });
82
+ }
83
+
77
84
  export function buildNodesMessage(): NodeInfo[] {
78
85
  var peers = loadPeers();
79
86
  var config = loadConfig();
@@ -95,6 +102,7 @@ export function buildNodesMessage(): NodeInfo[] {
95
102
  };
96
103
 
97
104
  var remotes: NodeInfo[] = peers.map(function (peer) {
105
+ var peerProjects = getConnectedPeerProjects(peer.id);
98
106
  return {
99
107
  id: peer.id,
100
108
  name: peer.name,
@@ -103,7 +111,9 @@ export function buildNodesMessage(): NodeInfo[] {
103
111
  port: 0,
104
112
  online: connectedIds.has(peer.id),
105
113
  isLocal: false,
106
- projects: [],
114
+ projects: peerProjects.map(function (p) {
115
+ return { slug: p.slug, path: "", title: p.title, nodeId: peer.id };
116
+ }),
107
117
  };
108
118
  });
109
119
 
@@ -159,7 +169,7 @@ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
159
169
  token: parsed!.token,
160
170
  port: pairConfig.port,
161
171
  addresses: getAllAddresses().map(function (a) { return a.address + ":" + pairConfig.port; }),
162
- projects: [],
172
+ projects: getLocalProjectsList(),
163
173
  }));
164
174
  });
165
175
 
@@ -189,6 +199,7 @@ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
189
199
 
190
200
  connectToPeer(peer.id, peerAddr);
191
201
 
202
+ var remoteProjectsList = (data as any).projects ?? [];
192
203
  var nodeInfo: NodeInfo = {
193
204
  id: peer.id,
194
205
  name: peer.name,
@@ -197,7 +208,9 @@ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
197
208
  port: parsed!.port,
198
209
  online: true,
199
210
  isLocal: false,
200
- projects: [],
211
+ projects: remoteProjectsList.map(function (rp: { slug: string; title: string }) {
212
+ return { slug: rp.slug, path: "", title: rp.title, nodeId: peer.id };
213
+ }),
201
214
  };
202
215
  sendTo(clientId, { type: "mesh:paired", node: nodeInfo });
203
216
  broadcast({ type: "mesh:nodes", nodes: buildNodesMessage() });
@@ -230,7 +243,7 @@ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
230
243
  nodeId: identity.id,
231
244
  name: loadConfig().name,
232
245
  publicKey: identity.publicKey,
233
- projects: [],
246
+ projects: getLocalProjectsList(),
234
247
  });
235
248
  broadcast({ type: "mesh:nodes", nodes: buildNodesMessage() });
236
249
  return;
@@ -295,6 +295,33 @@ export function onPeerMessage(callback: (nodeId: string, msg: MeshMessage) => vo
295
295
  messageCallbacks.push(callback);
296
296
  }
297
297
 
298
+ export function getConnectedPeerProjects(nodeId: string): Array<{ slug: string; title: string }> {
299
+ var conn = connections.get(nodeId);
300
+ if (!conn || conn.ws.readyState !== WebSocket.OPEN) return [];
301
+ return conn.projects;
302
+ }
303
+
304
+ export function getAllRemoteProjects(localNodeId: string): Array<{ slug: string; path: string; title: string; nodeId: string; nodeName: string; isRemote: boolean }> {
305
+ var results: Array<{ slug: string; path: string; title: string; nodeId: string; nodeName: string; isRemote: boolean }> = [];
306
+ for (var [nodeId, conn] of connections) {
307
+ if (conn.ws.readyState !== WebSocket.OPEN) continue;
308
+ var peers = require("./peers") as typeof import("./peers");
309
+ var peer = peers.getPeer(nodeId);
310
+ var peerName = peer ? peer.name : nodeId;
311
+ for (var i = 0; i < conn.projects.length; i++) {
312
+ results.push({
313
+ slug: conn.projects[i].slug,
314
+ path: "",
315
+ title: conn.projects[i].title,
316
+ nodeId: nodeId,
317
+ nodeName: peerName,
318
+ isRemote: true,
319
+ });
320
+ }
321
+ }
322
+ return results;
323
+ }
324
+
298
325
  export function findNodeForProject(projectSlug: string): string | undefined {
299
326
  for (var [nodeId, conn] of connections) {
300
327
  if (conn.ws.readyState !== WebSocket.OPEN) {