@cryptiklemur/lattice 1.42.3 → 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.
package/client/src/App.tsx
CHANGED
|
@@ -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);
|
|
@@ -215,7 +216,9 @@ export function ProjectRail(props: ProjectRailProps) {
|
|
|
215
216
|
var ws = useWebSocket();
|
|
216
217
|
var sidebar = useSidebar();
|
|
217
218
|
var groups = groupProjectsBySlug(props.projects, props.nodes);
|
|
219
|
+
var localNode = props.nodes.find(function (n) { return n.isLocal; });
|
|
218
220
|
var remoteNodes = props.nodes.filter(function (n) { return !n.isLocal; });
|
|
221
|
+
var allMeshNodes = localNode ? [localNode].concat(remoteNodes) : remoteNodes;
|
|
219
222
|
var [contextMenu, setContextMenu] = useState<ContextMenuState>({
|
|
220
223
|
visible: false,
|
|
221
224
|
x: 0,
|
|
@@ -223,23 +226,29 @@ export function ProjectRail(props: ProjectRailProps) {
|
|
|
223
226
|
slug: null,
|
|
224
227
|
});
|
|
225
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
|
+
});
|
|
226
232
|
|
|
227
233
|
useEffect(
|
|
228
234
|
function () {
|
|
229
|
-
if (!contextMenu.visible) return;
|
|
235
|
+
if (!contextMenu.visible && !nodeMenu.visible) return;
|
|
230
236
|
|
|
231
237
|
function handleClick() {
|
|
232
238
|
setContextMenu(function (prev) { return { ...prev, visible: false }; });
|
|
239
|
+
setNodeMenu(function (prev) { return { ...prev, visible: false }; });
|
|
233
240
|
}
|
|
234
241
|
|
|
235
242
|
function handleKeyDown(e: KeyboardEvent) {
|
|
236
243
|
if (e.key === "Escape") {
|
|
237
244
|
setContextMenu(function (prev) { return { ...prev, visible: false }; });
|
|
245
|
+
setNodeMenu(function (prev) { return { ...prev, visible: false }; });
|
|
238
246
|
}
|
|
239
247
|
}
|
|
240
248
|
|
|
241
249
|
function handleScroll() {
|
|
242
250
|
setContextMenu(function (prev) { return { ...prev, visible: false }; });
|
|
251
|
+
setNodeMenu(function (prev) { return { ...prev, visible: false }; });
|
|
243
252
|
}
|
|
244
253
|
|
|
245
254
|
window.addEventListener("click", handleClick);
|
|
@@ -252,9 +261,17 @@ export function ProjectRail(props: ProjectRailProps) {
|
|
|
252
261
|
window.removeEventListener("scroll", handleScroll, true);
|
|
253
262
|
};
|
|
254
263
|
},
|
|
255
|
-
[contextMenu.visible]
|
|
264
|
+
[contextMenu.visible, nodeMenu.visible]
|
|
256
265
|
);
|
|
257
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
|
+
|
|
258
275
|
function handleContextMenu(e: React.MouseEvent, slug: string) {
|
|
259
276
|
var menuWidth = 160;
|
|
260
277
|
var menuHeight = 100;
|
|
@@ -320,18 +337,10 @@ export function ProjectRail(props: ProjectRailProps) {
|
|
|
320
337
|
})}
|
|
321
338
|
|
|
322
339
|
|
|
323
|
-
{groups.length > 0 &&
|
|
340
|
+
{groups.length > 0 && (
|
|
324
341
|
<div className="w-6 h-px bg-base-300 my-0.5 flex-shrink-0" />
|
|
325
342
|
)}
|
|
326
343
|
|
|
327
|
-
{remoteNodes.map(function (node) {
|
|
328
|
-
return (
|
|
329
|
-
<NodeIndicator key={node.id} node={node} />
|
|
330
|
-
);
|
|
331
|
-
})}
|
|
332
|
-
|
|
333
|
-
<div className="w-6 h-px bg-base-300 my-0.5 flex-shrink-0" />
|
|
334
|
-
|
|
335
344
|
<button
|
|
336
345
|
onClick={function () { sidebar.openAddProject(); }}
|
|
337
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"
|
|
@@ -340,6 +349,24 @@ export function ProjectRail(props: ProjectRailProps) {
|
|
|
340
349
|
<Plus size={18} />
|
|
341
350
|
</button>
|
|
342
351
|
|
|
352
|
+
{allMeshNodes.length > 0 && (
|
|
353
|
+
<div className="w-6 h-px bg-base-300 my-0.5 flex-shrink-0" />
|
|
354
|
+
)}
|
|
355
|
+
|
|
356
|
+
{allMeshNodes.map(function (node) {
|
|
357
|
+
return (
|
|
358
|
+
<NodeIndicator key={node.id} node={node} onContextMenu={handleNodeContextMenu} />
|
|
359
|
+
);
|
|
360
|
+
})}
|
|
361
|
+
|
|
362
|
+
<button
|
|
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"
|
|
366
|
+
>
|
|
367
|
+
<Plus size={12} />
|
|
368
|
+
</button>
|
|
369
|
+
|
|
343
370
|
<div className="flex-1" />
|
|
344
371
|
|
|
345
372
|
{contextMenu.visible && createPortal(
|
|
@@ -394,6 +421,59 @@ export function ProjectRail(props: ProjectRailProps) {
|
|
|
394
421
|
document.body
|
|
395
422
|
)}
|
|
396
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
|
+
|
|
397
477
|
</div>
|
|
398
478
|
);
|
|
399
479
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cryptiklemur/lattice",
|
|
3
|
-
"version": "1.
|
|
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>",
|