@cryptiklemur/lattice 1.43.2 → 1.43.4

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.2",
3
+ "version": "1.43.4",
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>",
@@ -34,7 +34,9 @@ registerHandler("session", function (clientId: string, message: ClientMessage) {
34
34
  var listReqMsg = message as SessionListRequestMessage;
35
35
  var offset = listReqMsg.offset || 0;
36
36
  var limit = listReqMsg.limit || 0;
37
+ var t0 = Date.now();
37
38
  void listSessions(listReqMsg.projectSlug, { offset, limit }).then(function (result) {
39
+ log.session("session:list_request for %s took %dms (%d sessions)", listReqMsg.projectSlug, Date.now() - t0, result.sessions.length);
38
40
  sendTo(clientId, {
39
41
  type: "session:list",
40
42
  projectSlug: listReqMsg.projectSlug,
@@ -221,7 +221,7 @@ export function getActiveSessionCountForProject(projectPath: string): number {
221
221
  if (existsSync(join(dir, sessionId + ".jsonl"))) count++;
222
222
  }
223
223
 
224
- if (cliSessionsByProject.get(projectPath) !== null && cliSessionsByProject.get(projectPath) !== undefined) count++;
224
+ if (cliActiveProjects.has(projectPath)) count++;
225
225
 
226
226
  return count;
227
227
  }
@@ -241,28 +241,15 @@ export function isSessionBusy(sessionId: string): boolean {
241
241
  * The SDK spawns child processes (e.g. claude-agent-sdk/cli.js) that hold
242
242
  * lock files — those are NOT external.
243
243
  */
244
- var cliSessionsByProject = new Map<string, string | null>();
245
- var sessionNameCache = new Map<string, string>();
244
+ var cliActiveProjects = new Set<string>();
246
245
 
247
246
  function refreshCliDetection(): void {
248
- var config = loadConfig();
247
+ var newActive = new Set<string>();
249
248
  var cliPids = getClaudeCliPidsAsync();
250
- for (var i = 0; i < config.projects.length; i++) {
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
- found = resolveSessionName(projectPath, cmdline[resumeIdx + 1]);
259
- } else {
260
- found = findMostRecentSession(projectPath);
261
- }
262
- break;
263
- }
264
- cliSessionsByProject.set(projectPath, found);
249
+ for (var i = 0; i < cliPids.length; i++) {
250
+ newActive.add(cliPids[i].cwd);
265
251
  }
252
+ cliActiveProjects = newActive;
266
253
  }
267
254
 
268
255
  function getClaudeCliPidsAsync(): Array<{ pid: number; cwd: string; cmdline: string[] }> {
@@ -284,8 +271,7 @@ function getClaudeCliPidsAsync(): Array<{ pid: number; cwd: string; cmdline: str
284
271
  return results;
285
272
  }
286
273
 
287
- setInterval(refreshCliDetection, 5000);
288
- setTimeout(refreshCliDetection, 1000);
274
+ setInterval(refreshCliDetection, 10000);
289
275
 
290
276
  function getProjectPathForSession(sessionId: string): string | null {
291
277
  var config = loadConfig();
@@ -298,67 +284,11 @@ function getProjectPathForSession(sessionId: string): string | null {
298
284
  }
299
285
 
300
286
 
301
- function resolveSessionName(projectPath: string, name: string): string | null {
302
- var cacheKey = projectPath + ":" + name;
303
- var cached = sessionNameCache.get(cacheKey);
304
- if (cached) return cached;
305
-
306
- var hash = projectPath.replace(/\//g, "-");
307
- var dir = join(homedir(), ".claude", "projects", hash);
308
- if (!existsSync(dir)) return null;
309
-
310
- if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(name)) {
311
- if (existsSync(join(dir, name + ".jsonl"))) {
312
- sessionNameCache.set(cacheKey, name);
313
- return name;
314
- }
315
- }
316
-
317
- var entries = readdirSync(dir).filter(function (f) { return f.endsWith(".jsonl"); });
318
- for (var e = 0; e < entries.length; e++) {
319
- try {
320
- var content = readFileSync(join(dir, entries[e]), "utf-8");
321
- var titleIdx = content.indexOf('"custom-title"');
322
- if (titleIdx === -1) continue;
323
- var lineStart = content.lastIndexOf("\n", titleIdx) + 1;
324
- var lineEnd = content.indexOf("\n", titleIdx);
325
- var line = content.slice(lineStart, lineEnd === -1 ? undefined : lineEnd);
326
- var parsed = JSON.parse(line);
327
- if (parsed.type === "custom-title" && parsed.customTitle === name) {
328
- var id = entries[e].replace(".jsonl", "");
329
- sessionNameCache.set(cacheKey, id);
330
- return id;
331
- }
332
- } catch {}
333
- }
334
- return null;
335
- }
336
-
337
- function findMostRecentSession(projectPath: string): string | null {
338
- var hash = projectPath.replace(/\//g, "-");
339
- var dir = join(homedir(), ".claude", "projects", hash);
340
- if (!existsSync(dir)) return null;
341
-
342
- var entries = readdirSync(dir).filter(function (f) { return f.endsWith(".jsonl"); });
343
- var latest: { id: string; mtime: number } | null = null;
344
- for (var e = 0; e < entries.length; e++) {
345
- try {
346
- var s = statSync(join(dir, entries[e]));
347
- if (!latest || s.mtimeMs > latest.mtime) {
348
- latest = { id: entries[e].replace(".jsonl", ""), mtime: s.mtimeMs };
349
- }
350
- } catch {}
351
- }
352
- if (latest && Date.now() - latest.mtime < 60000) return latest.id;
353
- return null;
354
- }
355
-
356
-
357
287
  function isSessionLockedByExternal(sessionId: string): boolean {
358
288
  if (activeStreams.has(sessionId)) return false;
359
289
  var projectPath = getProjectPathForSession(sessionId);
360
290
  if (!projectPath) return false;
361
- return cliSessionsByProject.get(projectPath) === sessionId;
291
+ return cliActiveProjects.has(projectPath);
362
292
  }
363
293
 
364
294
  export function stopExternalSession(sessionId: string): boolean {