@cryptiklemur/lattice 1.42.4 → 1.43.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.
@@ -1,3 +1,4 @@
1
+ import { useEffect } from "react";
1
2
  import { RouterProvider } from "@tanstack/react-router";
2
3
  import { router } from "./router";
3
4
  import { WebSocketProvider } from "./providers/WebSocketProvider";
@@ -10,6 +11,15 @@ import { UpdatePrompt } from "./components/ui/UpdatePrompt";
10
11
  function AppInner() {
11
12
  var { items, dismiss } = useToastState();
12
13
 
14
+ useEffect(function () {
15
+ function blockContextMenu(e: MouseEvent) {
16
+ if ((e.target as HTMLElement).closest("[data-allow-context-menu]")) return;
17
+ e.preventDefault();
18
+ }
19
+ document.addEventListener("contextmenu", blockContextMenu);
20
+ return function () { document.removeEventListener("contextmenu", blockContextMenu); };
21
+ }, []);
22
+
13
23
  return (
14
24
  <>
15
25
  <RouterProvider router={router} />
@@ -478,6 +478,7 @@ export function ChatInput(props: ChatInputProps) {
478
478
  <span className="absolute left-0 top-[1px] text-primary/50 font-mono text-[14px] leading-relaxed select-none pointer-events-none">›</span>
479
479
  <textarea
480
480
  ref={textareaRef}
481
+ data-allow-context-menu
481
482
  aria-label="Message input"
482
483
  placeholder={props.disabled ? (props.disabledPlaceholder || "Claude is responding...") : "Message Claude..."}
483
484
  disabled={props.disabled}
@@ -185,7 +185,7 @@ function parseSkillInvocation(text: string): { skillName: string; content: strin
185
185
  function SkillMessage(props: { skillName: string; content: string; time: string | null; uuid?: string }) {
186
186
  var [expanded, setExpanded] = useState(false);
187
187
  return (
188
- <div id={props.uuid ? "msg-" + props.uuid : undefined} className="chat chat-end px-5 py-1 group/msg">
188
+ <div id={props.uuid ? "msg-" + props.uuid : undefined} data-allow-context-menu className="chat chat-end px-5 py-1 group/msg">
189
189
  <div className="chat-bubble chat-bubble-primary text-[13px] leading-relaxed break-words max-w-[95%] sm:max-w-[85%] shadow-sm">
190
190
  <button
191
191
  type="button"
@@ -230,7 +230,7 @@ function UserMessage(props: { message: HistoryMessage }) {
230
230
  return <SkillMessage skillName={skill.skillName} content={skill.content} time={time} uuid={msg.uuid} />;
231
231
  }
232
232
  return (
233
- <div id={msg.uuid ? "msg-" + msg.uuid : undefined} className="chat chat-end px-5 py-1 group/msg">
233
+ <div id={msg.uuid ? "msg-" + msg.uuid : undefined} data-allow-context-menu className="chat chat-end px-5 py-1 group/msg">
234
234
  <div className="chat-bubble chat-bubble-primary text-[13px] leading-relaxed break-words max-w-[95%] sm:max-w-[85%] shadow-sm">
235
235
  <div className="prose prose-sm max-w-none [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 prose-headings:text-primary-content prose-p:text-primary-content prose-strong:text-primary-content prose-code:text-primary-content/80 prose-pre:bg-primary/20 prose-a:text-primary-content/90 prose-a:underline">
236
236
  <Markdown remarkPlugins={[remarkGfm]} components={mdComponents}>{text}</Markdown>
@@ -265,7 +265,7 @@ function AssistantMessage(props: { message: HistoryMessage; responseCost?: numbe
265
265
  var msg = props.message;
266
266
  var time = formatTime(msg.timestamp);
267
267
  return (
268
- <div id={msg.uuid ? "msg-" + msg.uuid : undefined} className="chat chat-start px-5 py-1 group/msg">
268
+ <div id={msg.uuid ? "msg-" + msg.uuid : undefined} data-allow-context-menu className="chat chat-start px-5 py-1 group/msg">
269
269
  <div className="chat-image">
270
270
  <div className="w-6 h-6 rounded-full bg-primary/15 border border-primary/20 flex items-center justify-center">
271
271
  <div className="w-2.5 h-2.5 rounded-full bg-primary" />
@@ -147,7 +147,7 @@ function ProjectButton(props: ProjectButtonProps) {
147
147
  );
148
148
  }
149
149
 
150
- function NodeIndicator({ node }: { node: NodeInfo }) {
150
+ function NodeIndicator({ node, onContextMenu }: { node: NodeInfo; onContextMenu: (e: React.MouseEvent, node: NodeInfo) => void }) {
151
151
  var [hovered, setHovered] = useState(false);
152
152
  var [tooltipTop, setTooltipTop] = useState(0);
153
153
  var sidebar = useSidebar();
@@ -157,6 +157,7 @@ function NodeIndicator({ node }: { node: NodeInfo }) {
157
157
  <div className="relative flex items-center">
158
158
  <button
159
159
  onClick={function () { sidebar.openSettings("nodes"); }}
160
+ onContextMenu={function (e) { e.preventDefault(); onContextMenu(e, node); }}
160
161
  onMouseEnter={function (e) {
161
162
  var rect = e.currentTarget.getBoundingClientRect();
162
163
  setTooltipTop(rect.top + rect.height / 2);
@@ -225,23 +226,29 @@ export function ProjectRail(props: ProjectRailProps) {
225
226
  slug: null,
226
227
  });
227
228
  var menuRef = useRef<HTMLDivElement>(null);
229
+ var [nodeMenu, setNodeMenu] = useState<{ visible: boolean; x: number; y: number; node: NodeInfo | null }>({
230
+ visible: false, x: 0, y: 0, node: null,
231
+ });
228
232
 
229
233
  useEffect(
230
234
  function () {
231
- if (!contextMenu.visible) return;
235
+ if (!contextMenu.visible && !nodeMenu.visible) return;
232
236
 
233
237
  function handleClick() {
234
238
  setContextMenu(function (prev) { return { ...prev, visible: false }; });
239
+ setNodeMenu(function (prev) { return { ...prev, visible: false }; });
235
240
  }
236
241
 
237
242
  function handleKeyDown(e: KeyboardEvent) {
238
243
  if (e.key === "Escape") {
239
244
  setContextMenu(function (prev) { return { ...prev, visible: false }; });
245
+ setNodeMenu(function (prev) { return { ...prev, visible: false }; });
240
246
  }
241
247
  }
242
248
 
243
249
  function handleScroll() {
244
250
  setContextMenu(function (prev) { return { ...prev, visible: false }; });
251
+ setNodeMenu(function (prev) { return { ...prev, visible: false }; });
245
252
  }
246
253
 
247
254
  window.addEventListener("click", handleClick);
@@ -254,9 +261,17 @@ export function ProjectRail(props: ProjectRailProps) {
254
261
  window.removeEventListener("scroll", handleScroll, true);
255
262
  };
256
263
  },
257
- [contextMenu.visible]
264
+ [contextMenu.visible, nodeMenu.visible]
258
265
  );
259
266
 
267
+ function handleNodeContextMenu(e: React.MouseEvent, node: NodeInfo) {
268
+ var cx = e.clientX;
269
+ var cy = e.clientY;
270
+ if (cx + 160 > window.innerWidth - 8) cx = window.innerWidth - 168;
271
+ if (cy + 100 > window.innerHeight - 8) cy = window.innerHeight - 108;
272
+ setNodeMenu({ visible: true, x: cx, y: cy, node: node });
273
+ }
274
+
260
275
  function handleContextMenu(e: React.MouseEvent, slug: string) {
261
276
  var menuWidth = 160;
262
277
  var menuHeight = 100;
@@ -322,24 +337,34 @@ export function ProjectRail(props: ProjectRailProps) {
322
337
  })}
323
338
 
324
339
 
325
- {groups.length > 0 && allMeshNodes.length > 0 && (
340
+ {groups.length > 0 && (
341
+ <div className="w-6 h-px bg-base-300 my-0.5 flex-shrink-0" />
342
+ )}
343
+
344
+ <button
345
+ onClick={function () { sidebar.openAddProject(); }}
346
+ className="w-[42px] h-[42px] flex items-center justify-center rounded-full border-2 border-dashed border-base-content/25 text-base-content/20 hover:border-base-content/40 hover:text-base-content/40 transition-colors duration-[120ms] flex-shrink-0 cursor-pointer"
347
+ title="Add project"
348
+ >
349
+ <Plus size={18} />
350
+ </button>
351
+
352
+ {allMeshNodes.length > 0 && (
326
353
  <div className="w-6 h-px bg-base-300 my-0.5 flex-shrink-0" />
327
354
  )}
328
355
 
329
356
  {allMeshNodes.map(function (node) {
330
357
  return (
331
- <NodeIndicator key={node.id} node={node} />
358
+ <NodeIndicator key={node.id} node={node} onContextMenu={handleNodeContextMenu} />
332
359
  );
333
360
  })}
334
361
 
335
- <div className="w-6 h-px bg-base-300 my-0.5 flex-shrink-0" />
336
-
337
362
  <button
338
- onClick={function () { sidebar.openAddProject(); }}
339
- className="w-[42px] h-[42px] flex items-center justify-center rounded-full border-2 border-dashed border-base-content/25 text-base-content/20 hover:border-base-content/40 hover:text-base-content/40 transition-colors duration-[120ms] flex-shrink-0 cursor-pointer"
340
- title="Add project"
363
+ onClick={function () { sidebar.openSettings("nodes"); }}
364
+ className="w-[26px] h-[26px] flex items-center justify-center rounded-full border border-dashed border-base-content/15 text-base-content/15 hover:border-base-content/30 hover:text-base-content/30 transition-colors duration-[120ms] flex-shrink-0 cursor-pointer"
365
+ title="Pair a node"
341
366
  >
342
- <Plus size={18} />
367
+ <Plus size={12} />
343
368
  </button>
344
369
 
345
370
  <div className="flex-1" />
@@ -396,6 +421,59 @@ export function ProjectRail(props: ProjectRailProps) {
396
421
  document.body
397
422
  )}
398
423
 
424
+ {nodeMenu.visible && nodeMenu.node && createPortal(
425
+ <div
426
+ role="menu"
427
+ aria-label="Node actions"
428
+ onClick={function (e) { e.stopPropagation(); }}
429
+ className="fixed z-[99999] bg-base-300 border border-base-content/20 rounded-lg shadow-2xl py-1 min-w-[160px]"
430
+ style={{ left: nodeMenu.x + "px", top: nodeMenu.y + "px" }}
431
+ >
432
+ <button
433
+ role="menuitem"
434
+ className="w-full text-left px-3 py-1.5 text-sm text-base-content hover:bg-base-content/10 transition-colors"
435
+ onClick={function () {
436
+ sidebar.openSettings("nodes");
437
+ setNodeMenu(function (prev) { return { ...prev, visible: false }; });
438
+ }}
439
+ >
440
+ Node Settings
441
+ </button>
442
+ {!nodeMenu.node.isLocal && (
443
+ <>
444
+ {!nodeMenu.node.online && (
445
+ <button
446
+ role="menuitem"
447
+ className="w-full text-left px-3 py-1.5 text-sm text-base-content hover:bg-base-content/10 transition-colors"
448
+ onClick={function () {
449
+ if (nodeMenu.node) {
450
+ ws.send({ type: "mesh:reconnect", nodeId: nodeMenu.node.id } as any);
451
+ }
452
+ setNodeMenu(function (prev) { return { ...prev, visible: false }; });
453
+ }}
454
+ >
455
+ Reconnect
456
+ </button>
457
+ )}
458
+ <div className="my-1 h-px bg-base-content/10" />
459
+ <button
460
+ role="menuitem"
461
+ className="w-full text-left px-3 py-1.5 text-sm text-error hover:bg-error/10 transition-colors"
462
+ onClick={function () {
463
+ if (nodeMenu.node) {
464
+ ws.send({ type: "mesh:unpair", nodeId: nodeMenu.node.id });
465
+ }
466
+ setNodeMenu(function (prev) { return { ...prev, visible: false }; });
467
+ }}
468
+ >
469
+ Unpair
470
+ </button>
471
+ </>
472
+ )}
473
+ </div>,
474
+ document.body
475
+ )}
476
+
399
477
  </div>
400
478
  );
401
479
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cryptiklemur/lattice",
3
- "version": "1.42.4",
3
+ "version": "1.43.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>",