@cryptiklemur/lattice 1.11.3 → 1.11.5

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.
@@ -83,7 +83,10 @@ function LoadingScreen() {
83
83
  var logoRight = centerX + logoHalf;
84
84
  var logoBottom = centerY + logoHalf;
85
85
 
86
- var primary = "oklch(55% 0.25 280)";
86
+ var computedPrimary = getComputedStyle(document.documentElement).getPropertyValue("--color-primary").trim();
87
+ var primary = computedPrimary ? "oklch(" + computedPrimary + ")" : "oklch(55% 0.25 280)";
88
+
89
+ var prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
87
90
 
88
91
  type Dot = { x: number; y: number; col: number; row: number; hidden: boolean; brightness: number };
89
92
  var dots: Dot[] = [];
@@ -123,6 +126,37 @@ function LoadingScreen() {
123
126
  return result;
124
127
  }
125
128
 
129
+ function drawStatic() {
130
+ ctx!.clearRect(0, 0, canvasSize, canvasSize);
131
+ for (var di = 0; di < dots.length; di++) {
132
+ var dot = dots[di];
133
+ if (dot.hidden) continue;
134
+ ctx!.beginPath();
135
+ ctx!.arc(dot.x, dot.y, dotRadius, 0, Math.PI * 2);
136
+ ctx!.fillStyle = primary;
137
+ ctx!.globalAlpha = 0.15;
138
+ ctx!.fill();
139
+ ctx!.globalAlpha = 1.0;
140
+ }
141
+ var squares = [
142
+ [logoLeft, logoTop],
143
+ [logoLeft + logoSquare + logoGap, logoTop],
144
+ [logoLeft, logoTop + logoSquare + logoGap],
145
+ [logoLeft + logoSquare + logoGap, logoTop + logoSquare + logoGap],
146
+ ];
147
+ for (var si = 0; si < squares.length; si++) {
148
+ ctx!.fillStyle = primary;
149
+ ctx!.globalAlpha = 0.9;
150
+ ctx!.fillRect(squares[si][0], squares[si][1], logoSquare, logoSquare);
151
+ ctx!.globalAlpha = 1.0;
152
+ }
153
+ }
154
+
155
+ if (prefersReducedMotion) {
156
+ drawStatic();
157
+ return;
158
+ }
159
+
126
160
  function animate() {
127
161
  now = performance.now();
128
162
  ctx!.clearRect(0, 0, canvasSize, canvasSize);
@@ -277,8 +311,17 @@ function RemoveProjectConfirm() {
277
311
  }
278
312
  })();
279
313
 
314
+ useEffect(function () {
315
+ if (!slug) return;
316
+ function handleKeyDown(e: KeyboardEvent) {
317
+ if (e.key === "Escape") sidebar.closeConfirmRemove();
318
+ }
319
+ document.addEventListener("keydown", handleKeyDown);
320
+ return function () { document.removeEventListener("keydown", handleKeyDown); };
321
+ }, [slug]);
322
+
280
323
  return (
281
- <div className="fixed inset-0 z-[9999] flex items-center justify-center">
324
+ <div className="fixed inset-0 z-[9999] flex items-center justify-center" role="dialog" aria-modal="true" aria-label="Remove Project">
282
325
  <div className="absolute inset-0 bg-black/50" onClick={sidebar.closeConfirmRemove} />
283
326
  <div className="relative bg-base-200 border border-base-content/15 rounded-2xl shadow-2xl w-full max-w-sm mx-4 overflow-hidden">
284
327
  <div className="px-5 py-4 border-b border-base-content/15">
@@ -369,6 +412,9 @@ function RootLayout() {
369
412
 
370
413
  return (
371
414
  <div className="flex w-full h-full overflow-hidden bg-base-100">
415
+ <a href="#main-content" className="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[99999] focus:px-4 focus:py-2 focus:bg-primary focus:text-primary-content focus:rounded-lg focus:text-sm focus:font-semibold">
416
+ Skip to content
417
+ </a>
372
418
  <LoadingScreen />
373
419
  <div className="drawer lg:drawer-open h-full w-full">
374
420
  <input
@@ -376,12 +422,12 @@ function RootLayout() {
376
422
  type="checkbox"
377
423
  className="drawer-toggle"
378
424
  checked={sidebar.drawerOpen}
379
- onChange={function () {}}
425
+ readOnly
380
426
  />
381
427
 
382
- <div className="drawer-content flex flex-col h-full min-w-0 overflow-hidden">
428
+ <main id="main-content" className="drawer-content flex flex-col h-full min-w-0 overflow-hidden">
383
429
  <Outlet />
384
- </div>
430
+ </main>
385
431
 
386
432
  <div ref={drawerSideRef} className="drawer-side z-50 h-full">
387
433
  <label
@@ -390,9 +436,9 @@ function RootLayout() {
390
436
  className="drawer-overlay"
391
437
  onClick={closeDrawer}
392
438
  />
393
- <div className="h-full w-full lg:w-[284px] flex flex-col overflow-hidden">
439
+ <nav aria-label="Sidebar navigation" className="h-full w-full lg:w-[284px] flex flex-col overflow-hidden">
394
440
  <Sidebar onSessionSelect={closeDrawer} />
395
- </div>
441
+ </nav>
396
442
  </div>
397
443
  </div>
398
444
  <NodeSettingsModal
@@ -408,24 +454,71 @@ function RootLayout() {
408
454
  );
409
455
  }
410
456
 
411
- function IndexPage() {
412
- var sidebar = useSidebar();
413
- if (sidebar.activeView.type === "dashboard") {
414
- return <DashboardView />;
457
+ import { Component } from "react";
458
+ import type { ReactNode, ErrorInfo } from "react";
459
+ import { AlertTriangle, RefreshCw } from "lucide-react";
460
+
461
+ class ViewErrorBoundary extends Component<{ children: ReactNode; viewName: string }, { hasError: boolean; error: Error | null }> {
462
+ constructor(props: { children: ReactNode; viewName: string }) {
463
+ super(props);
464
+ this.state = { hasError: false, error: null };
415
465
  }
416
- if (sidebar.activeView.type === "settings") {
417
- return <SettingsView />;
466
+ static getDerivedStateFromError(error: Error) {
467
+ return { hasError: true, error: error };
418
468
  }
419
- if (sidebar.activeView.type === "project-settings") {
420
- return <ProjectSettingsView />;
469
+ componentDidCatch(error: Error, info: ErrorInfo) {
470
+ console.error("[lattice] View error in " + this.props.viewName + ":", error, info.componentStack);
421
471
  }
422
- if (sidebar.activeView.type === "project-dashboard") {
423
- return <ProjectDashboardView />;
472
+ render() {
473
+ if (this.state.hasError) {
474
+ var self = this;
475
+ return (
476
+ <div className="flex flex-col items-center justify-center h-full bg-base-100 bg-lattice-grid gap-4 p-8">
477
+ <AlertTriangle size={32} className="text-warning/50" />
478
+ <div className="text-center max-w-[500px]">
479
+ <p className="text-[14px] font-mono text-base-content/60 mb-1">
480
+ Error in {this.props.viewName}
481
+ </p>
482
+ <p className="text-[12px] text-base-content/30 mb-4 font-mono break-all">
483
+ {this.state.error?.message || "Unknown error"}
484
+ </p>
485
+ <button
486
+ onClick={function () { self.setState({ hasError: false, error: null }); }}
487
+ className="btn btn-ghost btn-sm text-[12px] gap-1.5"
488
+ >
489
+ <RefreshCw size={12} />
490
+ Retry
491
+ </button>
492
+ </div>
493
+ </div>
494
+ );
495
+ }
496
+ return this.props.children;
424
497
  }
425
- if (sidebar.activeView.type === "analytics") {
426
- return <AnalyticsView />;
498
+ }
499
+
500
+ function IndexPage() {
501
+ var sidebar = useSidebar();
502
+ var viewName = sidebar.activeView.type;
503
+ var content;
504
+ if (viewName === "dashboard") {
505
+ content = <DashboardView />;
506
+ } else if (viewName === "settings") {
507
+ content = <SettingsView />;
508
+ } else if (viewName === "project-settings") {
509
+ content = <ProjectSettingsView />;
510
+ } else if (viewName === "project-dashboard") {
511
+ content = <ProjectDashboardView />;
512
+ } else if (viewName === "analytics") {
513
+ content = <AnalyticsView />;
514
+ } else {
515
+ content = <WorkspaceView />;
427
516
  }
428
- return <WorkspaceView />;
517
+ return (
518
+ <ViewErrorBoundary viewName={viewName}>
519
+ {content}
520
+ </ViewErrorBoundary>
521
+ );
429
522
  }
430
523
 
431
524
  var rootRoute = createRootRoute({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cryptiklemur/lattice",
3
- "version": "1.11.3",
3
+ "version": "1.11.5",
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>",
@@ -15,7 +15,7 @@ import type { ClientMessage, MeshMessage } from "@lattice/shared";
15
15
  import "./handlers/session";
16
16
  import "./handlers/chat";
17
17
  import "./handlers/attachment";
18
- import { loadInterruptedSessions, unwatchSessionLock } from "./project/sdk-bridge";
18
+ import { loadInterruptedSessions, unwatchSessionLock, cleanupClientPermissions } from "./project/sdk-bridge";
19
19
  import { clearActiveSession, getActiveSession } from "./handlers/chat";
20
20
  import "./handlers/fs";
21
21
  import "./handlers/terminal";
@@ -312,6 +312,7 @@ export async function startDaemon(portOverride?: number | null): Promise<void> {
312
312
  removeClient(ws.data.id);
313
313
  cleanupClientTerminals(ws.data.id);
314
314
  cleanupClientAttachments(ws.data.id);
315
+ cleanupClientPermissions(ws.data.id);
315
316
  console.log(`[lattice] Client disconnected: ${ws.data.id}`);
316
317
  },
317
318
  },
@@ -141,6 +141,22 @@ export function deletePendingPermission(requestId: string): void {
141
141
  pendingPermissions.delete(requestId);
142
142
  }
143
143
 
144
+ export function cleanupClientPermissions(clientId: string): void {
145
+ var toRemove: string[] = [];
146
+ pendingPermissions.forEach(function (entry, requestId) {
147
+ if (entry.clientId === clientId) {
148
+ toRemove.push(requestId);
149
+ entry.resolve({ behavior: "deny", message: "Client disconnected.", toolUseID: entry.toolUseID });
150
+ }
151
+ });
152
+ for (var i = 0; i < toRemove.length; i++) {
153
+ pendingPermissions.delete(toRemove[i]);
154
+ }
155
+ if (toRemove.length > 0) {
156
+ console.log("[lattice] Cleaned up " + toRemove.length + " pending permission(s) for disconnected client " + clientId);
157
+ }
158
+ }
159
+
144
160
  export function addAutoApprovedTool(sessionId: string, toolName: string): void {
145
161
  var tools = autoApprovedTools.get(sessionId);
146
162
  if (!tools) {
@@ -423,6 +439,13 @@ export function startChatStream(options: ChatStreamOptions): void {
423
439
  }
424
440
  }
425
441
 
442
+ if (toolName === "Bash") {
443
+ var cmd = ((input.command || "") as string).trim();
444
+ if (cmd.startsWith("cd ")) {
445
+ return Promise.resolve({ behavior: "allow", updatedInput: input, toolUseID: options.toolUseID } as PermissionResult);
446
+ }
447
+ }
448
+
426
449
  var allowRules: string[] = [];
427
450
  if (existsSync(projectSettingsPath)) {
428
451
  try {