@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
|
-
|
|
290
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
269
|
-
var
|
|
270
|
-
|
|
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
|
|
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
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
|
296
|
-
var
|
|
297
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
367
|
-
for (var i = 0; i <
|
|
368
|
-
if (
|
|
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(
|
|
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
|
});
|