@cryptiklemur/lattice 1.36.0 → 1.36.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.
@@ -254,6 +254,18 @@ export function ProjectRail(props: ProjectRailProps) {
254
254
  );
255
255
  })}
256
256
 
257
+ {props.nodes.filter(function (n) {
258
+ return !n.isLocal && n.online && n.projects.length === 0;
259
+ }).map(function (n) {
260
+ return (
261
+ <div
262
+ key={"skeleton-" + n.id}
263
+ className="w-[42px] h-[42px] rounded-full bg-base-200 animate-pulse flex-shrink-0"
264
+ title={"Loading projects from " + n.name + "..."}
265
+ />
266
+ );
267
+ })}
268
+
257
269
  {groups.length > 0 && (
258
270
  <div className="w-6 h-px bg-base-300 my-0.5 flex-shrink-0" />
259
271
  )}
@@ -351,17 +351,20 @@ export function Sidebar({ onSessionSelect }: { onSessionSelect?: () => void }) {
351
351
 
352
352
  <div className="flex flex-col gap-0.5 mx-3 mt-1">
353
353
  {[
354
- { type: "files" as const, icon: FolderOpen, label: "Files" },
355
- { type: "terminal" as const, icon: TerminalSquare, label: "Terminal" },
356
- { type: "notes" as const, icon: StickyNote, label: "Notes" },
357
- { type: "tasks" as const, icon: Calendar, label: "Tasks" },
358
- { type: "bookmarks" as const, icon: Bookmark, label: "Bookmarks" },
354
+ { type: "files" as const, icon: FolderOpen, label: "Files", localOnly: true },
355
+ { type: "terminal" as const, icon: TerminalSquare, label: "Terminal", localOnly: true },
356
+ { type: "notes" as const, icon: StickyNote, label: "Notes", localOnly: false },
357
+ { type: "tasks" as const, icon: Calendar, label: "Tasks", localOnly: false },
358
+ { type: "bookmarks" as const, icon: Bookmark, label: "Bookmarks", localOnly: false },
359
359
  ].map(function (item) {
360
+ var isDisabled = item.localOnly && activeProject?.isRemote;
360
361
  return (
361
362
  <button
362
363
  key={item.type}
363
364
  type="button"
365
+ disabled={!!isDisabled}
364
366
  onClick={function () {
367
+ if (isDisabled) return;
365
368
  openTab(item.type);
366
369
  var state = getSidebarStore().state;
367
370
  if (state.activeView.type !== "chat") {
@@ -370,7 +373,13 @@ export function Sidebar({ onSessionSelect }: { onSessionSelect?: () => void }) {
370
373
  });
371
374
  }
372
375
  }}
373
- className="flex items-center gap-2 px-2 py-1.5 rounded-lg text-[11px] text-base-content/40 hover:text-base-content/70 hover:bg-base-300/30 transition-colors"
376
+ className={
377
+ "flex items-center gap-2 px-2 py-1.5 rounded-lg text-[11px] transition-colors " +
378
+ (isDisabled
379
+ ? "text-base-content/15 cursor-not-allowed"
380
+ : "text-base-content/40 hover:text-base-content/70 hover:bg-base-300/30")
381
+ }
382
+ title={isDisabled ? "Not available for remote projects" : undefined}
374
383
  >
375
384
  <item.icon size={12} />
376
385
  <span className="font-mono tracking-wide">{item.label}</span>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cryptiklemur/lattice",
3
- "version": "1.36.0",
3
+ "version": "1.36.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>",
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import type { ClientMessage, MeshProxyRequestMessage, MeshProxyResponseMessage, ServerMessage } from "@lattice/shared";
3
3
  import { getPeerConnection } from "./connector";
4
- import { sendTo, broadcast } from "../ws/broadcast";
4
+ import { sendTo, broadcast, registerVirtualClient, removeVirtualClient } from "../ws/broadcast";
5
5
  import { routeMessage } from "../ws/router";
6
6
 
7
7
  var pendingRequests = new Map<string, string>();
@@ -30,29 +30,26 @@ export function proxyToRemoteNode(nodeId: string, projectSlug: string, clientId:
30
30
  export function handleProxyRequest(sourceNodeId: string, msg: MeshProxyRequestMessage): void {
31
31
  var proxyClientId = "mesh-proxy:" + sourceNodeId + ":" + msg.requestId;
32
32
 
33
- var originalBroadcast = broadcast;
34
- void originalBroadcast;
35
-
36
- var interceptedSendTo = function (targetId: string, response: object): void {
37
- if (targetId === proxyClientId) {
38
- var ws = getPeerConnection(sourceNodeId);
39
- if (!ws) {
40
- console.warn("[mesh/proxy] Cannot send response, no connection to: " + sourceNodeId);
41
- return;
42
- }
43
-
44
- var envelope: MeshProxyResponseMessage = {
45
- type: "mesh:proxy_response",
46
- projectSlug: msg.projectSlug,
47
- requestId: msg.requestId,
48
- payload: response as ServerMessage,
49
- };
50
-
51
- ws.send(JSON.stringify(envelope));
33
+ registerVirtualClient(proxyClientId, function (response: object) {
34
+ var ws = getPeerConnection(sourceNodeId);
35
+ if (!ws) {
36
+ console.warn("[mesh/proxy] Cannot send response, no connection to: " + sourceNodeId);
37
+ removeVirtualClient(proxyClientId);
38
+ return;
52
39
  }
53
- };
54
40
 
55
- proxyRouteMessage(proxyClientId, msg.payload, interceptedSendTo);
41
+ var envelope: MeshProxyResponseMessage = {
42
+ type: "mesh:proxy_response",
43
+ projectSlug: msg.projectSlug,
44
+ requestId: msg.requestId,
45
+ payload: response as ServerMessage,
46
+ };
47
+
48
+ ws.send(JSON.stringify(envelope));
49
+ removeVirtualClient(proxyClientId);
50
+ });
51
+
52
+ routeMessage(proxyClientId, msg.payload);
56
53
  }
57
54
 
58
55
  export function handleProxyResponse(msg: MeshProxyResponseMessage): void {
@@ -1,6 +1,15 @@
1
1
  import type { ServerWebSocket } from "bun";
2
2
 
3
3
  var clients = new Map<string, ServerWebSocket<{ id: string }>>();
4
+ var virtualSendHandlers = new Map<string, (message: object) => void>();
5
+
6
+ export function registerVirtualClient(id: string, handler: (message: object) => void): void {
7
+ virtualSendHandlers.set(id, handler);
8
+ }
9
+
10
+ export function removeVirtualClient(id: string): void {
11
+ virtualSendHandlers.delete(id);
12
+ }
4
13
 
5
14
  export function addClient(ws: ServerWebSocket<{ id: string }>): void {
6
15
  clients.set(ws.data.id, ws);
@@ -23,6 +32,11 @@ export function sendTo(id: string, message: object): void {
23
32
  var ws = clients.get(id);
24
33
  if (ws) {
25
34
  ws.send(JSON.stringify(message));
35
+ return;
36
+ }
37
+ var virtualHandler = virtualSendHandlers.get(id);
38
+ if (virtualHandler) {
39
+ virtualHandler(message);
26
40
  }
27
41
  }
28
42
 
@@ -56,17 +56,18 @@ export function routeMessage(clientId: string, message: ClientMessage): void {
56
56
  if (PROXIED_PREFIXES.has(prefix)) {
57
57
  var remote = clientRemoteNode.get(clientId);
58
58
 
59
- if (message.type === "session:activate") {
60
- var activateMsg = message as { type: string; projectSlug: string; sessionId: string };
61
- var localProject = getLocalProject(activateMsg.projectSlug);
59
+ var msgSlug = (message as any).projectSlug as string | undefined;
60
+
61
+ if (msgSlug) {
62
+ var localProject = getLocalProject(msgSlug);
62
63
  if (!localProject) {
63
- var remoteEntry = getRemoteNodeForProject(activateMsg.projectSlug);
64
+ var remoteEntry = getRemoteNodeForProject(msgSlug);
64
65
  if (remoteEntry) {
65
- setClientRemoteNode(clientId, remoteEntry.nodeId, activateMsg.projectSlug);
66
- proxyMessage(clientId, remoteEntry.nodeId, activateMsg.projectSlug, message);
66
+ setClientRemoteNode(clientId, remoteEntry.nodeId, msgSlug);
67
+ proxyMessage(clientId, remoteEntry.nodeId, msgSlug, message);
67
68
  return;
68
69
  }
69
- } else {
70
+ } else if (message.type === "session:activate" || message.type === "session:list_request") {
70
71
  clearClientRemoteNode(clientId);
71
72
  }
72
73
  } else if (remote) {