@cryptiklemur/lattice 1.43.1 → 1.43.3

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.
@@ -175,10 +175,25 @@ function PreviewPopover(props: { preview: SessionPreview | null; anchorRect: DOM
175
175
  return createPortal(content, document.body);
176
176
  }
177
177
 
178
+ function loadCachedSessions(slug: string | null): SessionSummary[] {
179
+ if (!slug) return [];
180
+ try {
181
+ var raw = localStorage.getItem("lattice:sessions:" + slug);
182
+ if (raw) return JSON.parse(raw);
183
+ } catch {}
184
+ return [];
185
+ }
186
+
187
+ function cacheSessions(slug: string, sessions: SessionSummary[]): void {
188
+ try {
189
+ localStorage.setItem("lattice:sessions:" + slug, JSON.stringify(sessions.slice(0, 100)));
190
+ } catch {}
191
+ }
192
+
178
193
  export function SessionList(props: SessionListProps) {
179
194
  useTimeTick();
180
195
  var ws = useWebSocket();
181
- var [sessions, setSessions] = useState<SessionSummary[]>([]);
196
+ var [sessions, setSessions] = useState<SessionSummary[]>(function () { return loadCachedSessions(props.projectSlug); });
182
197
  var [loading, setLoading] = useState<boolean>(false);
183
198
  var [loadingMore, setLoadingMore] = useState<boolean>(false);
184
199
  var [totalCount, setTotalCount] = useState<number>(0);
@@ -240,6 +255,9 @@ export function SessionList(props: SessionListProps) {
240
255
 
241
256
  setTotalCount(listTotal);
242
257
  offsetRef.current = listOffset + incoming.length;
258
+ if (props.projectSlug && listOffset === 0) {
259
+ cacheSessions(props.projectSlug, incoming);
260
+ }
243
261
  hasMoreRef.current = listOffset + incoming.length < listTotal;
244
262
  }
245
263
  } else if (msg.type === "session:created") {
@@ -286,8 +304,9 @@ export function SessionList(props: SessionListProps) {
286
304
 
287
305
  useEffect(function () {
288
306
  if (props.projectSlug && ws.status === "connected") {
289
- setSessions([]);
290
- setLoading(true);
307
+ var cached = loadCachedSessions(props.projectSlug);
308
+ setSessions(cached);
309
+ setLoading(cached.length === 0);
291
310
  offsetRef.current = 0;
292
311
  hasMoreRef.current = true;
293
312
  sendRef.current({ type: "session:list_request", projectSlug: props.projectSlug, offset: 0, limit: PAGE_SIZE });
@@ -12,9 +12,23 @@ export interface UseProjectsResult {
12
12
  setActiveProject: (project: ProjectInfo | null) => void;
13
13
  }
14
14
 
15
+ function loadCachedProjects(): ProjectInfo[] {
16
+ try {
17
+ var raw = localStorage.getItem("lattice:projects");
18
+ if (raw) return JSON.parse(raw);
19
+ } catch {}
20
+ return [];
21
+ }
22
+
23
+ function cacheProjects(projects: ProjectInfo[]): void {
24
+ try {
25
+ localStorage.setItem("lattice:projects", JSON.stringify(projects));
26
+ } catch {}
27
+ }
28
+
15
29
  export function useProjects(): UseProjectsResult {
16
30
  var ws = useWebSocket();
17
- var [projects, setProjects] = useState<ProjectInfo[]>([]);
31
+ var [projects, setProjects] = useState<ProjectInfo[]>(loadCachedProjects);
18
32
  var activeProjectSlug = useStore(getSidebarStore(), function (state) { return state.activeProjectSlug; });
19
33
 
20
34
  var handleRef = useRef<(msg: ServerMessage) => void>(function () {});
@@ -32,7 +46,9 @@ export function useProjects(): UseProjectsResult {
32
46
  for (var i = 0; i < kept.length; i++) {
33
47
  (kept[i] as any).online = false;
34
48
  }
35
- return incoming.concat(kept);
49
+ var merged = incoming.concat(kept);
50
+ cacheProjects(merged);
51
+ return merged;
36
52
  });
37
53
  var storeState = getSidebarStore().state;
38
54
  var currentSlug = storeState.activeProjectSlug;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cryptiklemur/lattice",
3
- "version": "1.43.1",
3
+ "version": "1.43.3",
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>",
@@ -221,32 +221,11 @@ export function getActiveSessionCountForProject(projectPath: string): number {
221
221
  if (existsSync(join(dir, sessionId + ".jsonl"))) count++;
222
222
  }
223
223
 
224
- for (var [sessionId2] of streamMetadata) {
225
- void sessionId2;
226
- }
227
-
228
- if (isClaudeCliRunningInProject(projectPath)) count++;
224
+ if (cliSessionsByProject.get(projectPath) !== null && cliSessionsByProject.get(projectPath) !== undefined) count++;
229
225
 
230
226
  return count;
231
227
  }
232
228
 
233
- function isClaudeCliRunningInProject(projectPath: string): boolean {
234
- try {
235
- var result = Bun.spawnSync(["pgrep", "-x", "claude"], { stderr: "ignore" });
236
- if (result.exitCode !== 0) return false;
237
- var pids = result.stdout.toString().trim().split("\n");
238
- for (var i = 0; i < pids.length; i++) {
239
- var pid = parseInt(pids[i], 10);
240
- if (isNaN(pid) || pid === process.pid) continue;
241
- try {
242
- var cwd = readlinkSync("/proc/" + pid + "/cwd");
243
- if (cwd === projectPath) return true;
244
- } catch {}
245
- }
246
- } catch {}
247
- return false;
248
- }
249
-
250
229
  /**
251
230
  * Check if a session is controlled by an external process (not Lattice).
252
231
  * Lattice's own active streams are handled by isProcessing on the client,
@@ -262,28 +241,46 @@ export function isSessionBusy(sessionId: string): boolean {
262
241
  * The SDK spawns child processes (e.g. claude-agent-sdk/cli.js) that hold
263
242
  * lock files — those are NOT external.
264
243
  */
265
- function getProjectPathForSession(sessionId: string): string | null {
244
+ var cliSessionsByProject = new Map<string, string | null>();
245
+ var sessionNameCache = new Map<string, string>();
246
+
247
+ function refreshCliDetection(): void {
266
248
  var config = loadConfig();
249
+ var cliPids = getClaudeCliPidsAsync();
267
250
  for (var i = 0; i < config.projects.length; i++) {
268
- var hash = config.projects[i].path.replace(/\//g, "-");
269
- var jsonlPath = join(homedir(), ".claude", "projects", hash, sessionId + ".jsonl");
270
- if (existsSync(jsonlPath)) return config.projects[i].path;
251
+ var projectPath = config.projects[i].path;
252
+ var found: string | null = null;
253
+ for (var j = 0; j < cliPids.length; j++) {
254
+ if (cliPids[j].cwd !== projectPath) continue;
255
+ var cmdline = cliPids[j].cmdline;
256
+ var resumeIdx = cmdline.indexOf("--resume");
257
+ if (resumeIdx !== -1 && resumeIdx + 1 < cmdline.length) {
258
+ var sessionName = cmdline[resumeIdx + 1];
259
+ found = resolveSessionName(projectPath, sessionName);
260
+ if (!found) {
261
+ resolveSessionNameAsync(projectPath, sessionName);
262
+ }
263
+ } else {
264
+ found = findMostRecentSession(projectPath);
265
+ }
266
+ break;
267
+ }
268
+ cliSessionsByProject.set(projectPath, found);
271
269
  }
272
- return null;
273
270
  }
274
271
 
275
- function getClaudeCliPids(): Array<{ pid: number; cwd: string; cmdline: string[] }> {
272
+ function getClaudeCliPidsAsync(): Array<{ pid: number; cwd: string; cmdline: string[] }> {
276
273
  var results: Array<{ pid: number; cwd: string; cmdline: string[] }> = [];
277
274
  try {
278
- var result = Bun.spawnSync(["pgrep", "-x", "claude"], { stderr: "ignore" });
279
- if (result.exitCode !== 0) return results;
280
- var pidStrs = result.stdout.toString().trim().split("\n");
281
- for (var i = 0; i < pidStrs.length; i++) {
282
- var pid = parseInt(pidStrs[i], 10);
283
- if (isNaN(pid) || pid === process.pid) continue;
275
+ var procEntries = readdirSync("/proc").filter(function (e) { return /^\d+$/.test(e); });
276
+ for (var i = 0; i < procEntries.length; i++) {
277
+ var pid = parseInt(procEntries[i], 10);
278
+ if (pid === process.pid) continue;
284
279
  try {
285
- var cwd = readlinkSync("/proc/" + pid + "/cwd");
286
280
  var cmdline = readFileSync("/proc/" + pid + "/cmdline", "utf-8").split("\0");
281
+ var exe = cmdline[0] || "";
282
+ if (!exe.endsWith("/claude") && exe !== "claude") continue;
283
+ var cwd = readlinkSync("/proc/" + pid + "/cwd");
287
284
  results.push({ pid, cwd, cmdline });
288
285
  } catch {}
289
286
  }
@@ -291,31 +288,61 @@ function getClaudeCliPids(): Array<{ pid: number; cwd: string; cmdline: string[]
291
288
  return results;
292
289
  }
293
290
 
291
+ setInterval(refreshCliDetection, 5000);
292
+ setTimeout(refreshCliDetection, 1000);
293
+
294
+ function getProjectPathForSession(sessionId: string): string | null {
295
+ var config = loadConfig();
296
+ for (var i = 0; i < config.projects.length; i++) {
297
+ var hash = config.projects[i].path.replace(/\//g, "-");
298
+ var jsonlPath = join(homedir(), ".claude", "projects", hash, sessionId + ".jsonl");
299
+ if (existsSync(jsonlPath)) return config.projects[i].path;
300
+ }
301
+ return null;
302
+ }
303
+
304
+
294
305
  function resolveSessionName(projectPath: string, name: string): string | null {
295
- var hash = projectPath.replace(/\//g, "-");
296
- var dir = join(homedir(), ".claude", "projects", hash);
297
- if (!existsSync(dir)) return null;
306
+ var cacheKey = projectPath + ":" + name;
307
+ var cached = sessionNameCache.get(cacheKey);
308
+ if (cached) return cached;
298
309
 
299
310
  if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(name)) {
300
- if (existsSync(join(dir, name + ".jsonl"))) return name;
311
+ var hash = projectPath.replace(/\//g, "-");
312
+ if (existsSync(join(homedir(), ".claude", "projects", hash, name + ".jsonl"))) {
313
+ sessionNameCache.set(cacheKey, name);
314
+ return name;
315
+ }
301
316
  }
302
317
 
303
- var entries = readdirSync(dir).filter(function (f) { return f.endsWith(".jsonl"); });
304
- for (var e = 0; e < entries.length; e++) {
305
- try {
306
- var result = Bun.spawnSync(["grep", "-m", "1", "custom-title", join(dir, entries[e])], { stdout: "pipe", stderr: "ignore" });
307
- if (result.exitCode !== 0) continue;
308
- var line = result.stdout.toString().trim();
309
- if (!line) continue;
310
- var parsed = JSON.parse(line);
311
- if (parsed.type === "custom-title" && parsed.customTitle === name) {
312
- return entries[e].replace(".jsonl", "");
313
- }
314
- } catch {}
315
- }
316
318
  return null;
317
319
  }
318
320
 
321
+ function resolveSessionNameAsync(projectPath: string, name: string): void {
322
+ var cacheKey = projectPath + ":" + name;
323
+ if (sessionNameCache.has(cacheKey)) return;
324
+
325
+ var hash = projectPath.replace(/\//g, "-");
326
+ var dir = join(homedir(), ".claude", "projects", hash);
327
+ if (!existsSync(dir)) return;
328
+
329
+ var proc = Bun.spawn(["grep", "-rl", "--include=*.jsonl", "-m", "1", name, dir], {
330
+ stdout: "pipe", stderr: "ignore",
331
+ });
332
+ void proc.exited.then(function () {
333
+ var output = new Response(proc.stdout).text();
334
+ return output;
335
+ }).then(function (text) {
336
+ var files = text.trim().split("\n").filter(Boolean);
337
+ if (files.length > 0) {
338
+ var match = files[0].match(/([0-9a-f-]{36})\.jsonl$/);
339
+ if (match) {
340
+ sessionNameCache.set(cacheKey, match[1]);
341
+ }
342
+ }
343
+ }).catch(function () {});
344
+ }
345
+
319
346
  function findMostRecentSession(projectPath: string): string | null {
320
347
  var hash = projectPath.replace(/\//g, "-");
321
348
  var dir = join(homedir(), ".claude", "projects", hash);
@@ -335,39 +362,22 @@ function findMostRecentSession(projectPath: string): string | null {
335
362
  return null;
336
363
  }
337
364
 
338
- function getCliSessionIdForProject(projectPath: string): string | null {
339
- var cliProcesses = getClaudeCliPids();
340
- for (var i = 0; i < cliProcesses.length; i++) {
341
- if (cliProcesses[i].cwd !== projectPath) continue;
342
-
343
- var cmdline = cliProcesses[i].cmdline;
344
- var resumeIdx = cmdline.indexOf("--resume");
345
- if (resumeIdx !== -1 && resumeIdx + 1 < cmdline.length) {
346
- var sessionName = cmdline[resumeIdx + 1];
347
- return resolveSessionName(projectPath, sessionName);
348
- }
349
-
350
- return findMostRecentSession(projectPath);
351
- }
352
- return null;
353
- }
354
365
 
355
366
  function isSessionLockedByExternal(sessionId: string): boolean {
356
367
  if (activeStreams.has(sessionId)) return false;
357
368
  var projectPath = getProjectPathForSession(sessionId);
358
369
  if (!projectPath) return false;
359
- var cliSessionId = getCliSessionIdForProject(projectPath);
360
- return cliSessionId === sessionId;
370
+ return cliSessionsByProject.get(projectPath) === sessionId;
361
371
  }
362
372
 
363
373
  export function stopExternalSession(sessionId: string): boolean {
364
374
  var projectPath = getProjectPathForSession(sessionId);
365
375
  if (!projectPath) return false;
366
- var cliProcesses = getClaudeCliPids();
367
- for (var i = 0; i < cliProcesses.length; i++) {
368
- if (cliProcesses[i].cwd === projectPath) {
376
+ var pids = getClaudeCliPidsAsync();
377
+ for (var i = 0; i < pids.length; i++) {
378
+ if (pids[i].cwd === projectPath) {
369
379
  try {
370
- process.kill(cliProcesses[i].pid, "SIGINT");
380
+ process.kill(pids[i].pid, "SIGINT");
371
381
  return true;
372
382
  } catch {}
373
383
  }
@@ -422,7 +422,9 @@ export async function listSessions(projectSlug: string, options?: { offset?: num
422
422
  }
423
423
 
424
424
  try {
425
+ var sdkT0 = Date.now();
425
426
  var sdkSessions = await sdkListSessions({ dir: projectPath });
427
+ log.session("sdkListSessions for %s: %dms (%d sessions)", projectSlug, Date.now() - sdkT0, sdkSessions.length);
426
428
  var summaries = sdkSessions.map(function (s) {
427
429
  return mapSDKSession(s, projectSlug);
428
430
  });