@cryptiklemur/lattice 1.42.4 → 1.43.1
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 +10 -0
- package/client/src/components/chat/ChatInput.tsx +1 -0
- package/client/src/components/chat/Message.tsx +3 -3
- package/client/src/components/sidebar/ProjectRail.tsx +89 -11
- package/client/src/components/sidebar/SessionList.tsx +1 -1
- package/package.json +1 -1
- package/server/src/handlers/session.ts +20 -16
- package/server/src/project/sdk-bridge.ts +4 -3
- package/server/src/project/session.ts +24 -4
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);
|
|
@@ -225,23 +226,29 @@ export function ProjectRail(props: ProjectRailProps) {
|
|
|
225
226
|
slug: null,
|
|
226
227
|
});
|
|
227
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
|
+
});
|
|
228
232
|
|
|
229
233
|
useEffect(
|
|
230
234
|
function () {
|
|
231
|
-
if (!contextMenu.visible) return;
|
|
235
|
+
if (!contextMenu.visible && !nodeMenu.visible) return;
|
|
232
236
|
|
|
233
237
|
function handleClick() {
|
|
234
238
|
setContextMenu(function (prev) { return { ...prev, visible: false }; });
|
|
239
|
+
setNodeMenu(function (prev) { return { ...prev, visible: false }; });
|
|
235
240
|
}
|
|
236
241
|
|
|
237
242
|
function handleKeyDown(e: KeyboardEvent) {
|
|
238
243
|
if (e.key === "Escape") {
|
|
239
244
|
setContextMenu(function (prev) { return { ...prev, visible: false }; });
|
|
245
|
+
setNodeMenu(function (prev) { return { ...prev, visible: false }; });
|
|
240
246
|
}
|
|
241
247
|
}
|
|
242
248
|
|
|
243
249
|
function handleScroll() {
|
|
244
250
|
setContextMenu(function (prev) { return { ...prev, visible: false }; });
|
|
251
|
+
setNodeMenu(function (prev) { return { ...prev, visible: false }; });
|
|
245
252
|
}
|
|
246
253
|
|
|
247
254
|
window.addEventListener("click", handleClick);
|
|
@@ -254,9 +261,17 @@ export function ProjectRail(props: ProjectRailProps) {
|
|
|
254
261
|
window.removeEventListener("scroll", handleScroll, true);
|
|
255
262
|
};
|
|
256
263
|
},
|
|
257
|
-
[contextMenu.visible]
|
|
264
|
+
[contextMenu.visible, nodeMenu.visible]
|
|
258
265
|
);
|
|
259
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
|
+
|
|
260
275
|
function handleContextMenu(e: React.MouseEvent, slug: string) {
|
|
261
276
|
var menuWidth = 160;
|
|
262
277
|
var menuHeight = 100;
|
|
@@ -322,24 +337,34 @@ export function ProjectRail(props: ProjectRailProps) {
|
|
|
322
337
|
})}
|
|
323
338
|
|
|
324
339
|
|
|
325
|
-
{groups.length > 0 &&
|
|
340
|
+
{groups.length > 0 && (
|
|
341
|
+
<div className="w-6 h-px bg-base-300 my-0.5 flex-shrink-0" />
|
|
342
|
+
)}
|
|
343
|
+
|
|
344
|
+
<button
|
|
345
|
+
onClick={function () { sidebar.openAddProject(); }}
|
|
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"
|
|
347
|
+
title="Add project"
|
|
348
|
+
>
|
|
349
|
+
<Plus size={18} />
|
|
350
|
+
</button>
|
|
351
|
+
|
|
352
|
+
{allMeshNodes.length > 0 && (
|
|
326
353
|
<div className="w-6 h-px bg-base-300 my-0.5 flex-shrink-0" />
|
|
327
354
|
)}
|
|
328
355
|
|
|
329
356
|
{allMeshNodes.map(function (node) {
|
|
330
357
|
return (
|
|
331
|
-
<NodeIndicator key={node.id} node={node} />
|
|
358
|
+
<NodeIndicator key={node.id} node={node} onContextMenu={handleNodeContextMenu} />
|
|
332
359
|
);
|
|
333
360
|
})}
|
|
334
361
|
|
|
335
|
-
<div className="w-6 h-px bg-base-300 my-0.5 flex-shrink-0" />
|
|
336
|
-
|
|
337
362
|
<button
|
|
338
|
-
onClick={function () { sidebar.
|
|
339
|
-
className="w-[
|
|
340
|
-
title="
|
|
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"
|
|
341
366
|
>
|
|
342
|
-
<Plus size={
|
|
367
|
+
<Plus size={12} />
|
|
343
368
|
</button>
|
|
344
369
|
|
|
345
370
|
<div className="flex-1" />
|
|
@@ -396,6 +421,59 @@ export function ProjectRail(props: ProjectRailProps) {
|
|
|
396
421
|
document.body
|
|
397
422
|
)}
|
|
398
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
|
+
|
|
399
477
|
</div>
|
|
400
478
|
);
|
|
401
479
|
}
|
|
@@ -295,7 +295,7 @@ export function SessionList(props: SessionListProps) {
|
|
|
295
295
|
if (props.projectSlug && ws.status === "connected") {
|
|
296
296
|
sendRef.current({ type: "session:list_request", projectSlug: props.projectSlug, offset: 0, limit: PAGE_SIZE });
|
|
297
297
|
}
|
|
298
|
-
},
|
|
298
|
+
}, 30000);
|
|
299
299
|
return function () { clearInterval(interval); };
|
|
300
300
|
}
|
|
301
301
|
}, [props.projectSlug, ws.status]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cryptiklemur/lattice",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.43.1",
|
|
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>",
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
SessionActivateMessage,
|
|
4
4
|
SessionCreateMessage,
|
|
5
5
|
SessionDeleteMessage,
|
|
6
|
+
SessionSummary,
|
|
6
7
|
SessionListRequestMessage,
|
|
7
8
|
SessionPreviewRequestMessage,
|
|
8
9
|
SessionRenameMessage,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
getSessionTitle,
|
|
19
20
|
getSessionUsage,
|
|
20
21
|
listSessions,
|
|
22
|
+
invalidateSessionCache,
|
|
21
23
|
loadSessionHistory,
|
|
22
24
|
renameSession,
|
|
23
25
|
} from "../project/session";
|
|
@@ -30,16 +32,14 @@ import { log } from "../logger";
|
|
|
30
32
|
registerHandler("session", function (clientId: string, message: ClientMessage) {
|
|
31
33
|
if (message.type === "session:list_request") {
|
|
32
34
|
var listReqMsg = message as SessionListRequestMessage;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
var totalCount = sessions.length;
|
|
37
|
-
var sliced = limit > 0 ? sessions.slice(offset, offset + limit) : sessions;
|
|
35
|
+
var offset = listReqMsg.offset || 0;
|
|
36
|
+
var limit = listReqMsg.limit || 0;
|
|
37
|
+
void listSessions(listReqMsg.projectSlug, { offset, limit }).then(function (result) {
|
|
38
38
|
sendTo(clientId, {
|
|
39
39
|
type: "session:list",
|
|
40
40
|
projectSlug: listReqMsg.projectSlug,
|
|
41
|
-
sessions:
|
|
42
|
-
totalCount,
|
|
41
|
+
sessions: result.sessions,
|
|
42
|
+
totalCount: result.totalCount,
|
|
43
43
|
offset,
|
|
44
44
|
});
|
|
45
45
|
});
|
|
@@ -59,16 +59,16 @@ registerHandler("session", function (clientId: string, message: ClientMessage) {
|
|
|
59
59
|
if (message.type === "session:list_all_request") {
|
|
60
60
|
var config = loadConfig();
|
|
61
61
|
var allPromises = config.projects.map(function (p: typeof config.projects[number]) {
|
|
62
|
-
return listSessions(p.slug);
|
|
62
|
+
return listSessions(p.slug, { limit: 20 });
|
|
63
63
|
});
|
|
64
64
|
void Promise.all(allPromises).then(function (results) {
|
|
65
|
-
var merged:
|
|
65
|
+
var merged: SessionSummary[] = [];
|
|
66
66
|
for (var i = 0; i < results.length; i++) {
|
|
67
|
-
for (var j = 0; j < results[i].length; j++) {
|
|
68
|
-
merged.push(results[i][j]);
|
|
67
|
+
for (var j = 0; j < results[i].sessions.length; j++) {
|
|
68
|
+
merged.push(results[i].sessions[j]);
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
-
merged.sort(function (a
|
|
71
|
+
merged.sort(function (a, b) { return b.updatedAt - a.updatedAt; });
|
|
72
72
|
sendTo(clientId, {
|
|
73
73
|
type: "session:list_all",
|
|
74
74
|
sessions: merged.slice(0, 20),
|
|
@@ -171,11 +171,13 @@ registerHandler("session", function (clientId: string, message: ClientMessage) {
|
|
|
171
171
|
return;
|
|
172
172
|
}
|
|
173
173
|
void renameSession(projectSlug, renameMsg.sessionId, renameMsg.title).then(function () {
|
|
174
|
-
|
|
174
|
+
invalidateSessionCache(projectSlug);
|
|
175
|
+
void listSessions(projectSlug, { limit: 40 }).then(function (result) {
|
|
175
176
|
sendTo(clientId, {
|
|
176
177
|
type: "session:list",
|
|
177
178
|
projectSlug,
|
|
178
|
-
sessions,
|
|
179
|
+
sessions: result.sessions,
|
|
180
|
+
totalCount: result.totalCount,
|
|
179
181
|
});
|
|
180
182
|
});
|
|
181
183
|
});
|
|
@@ -191,11 +193,13 @@ registerHandler("session", function (clientId: string, message: ClientMessage) {
|
|
|
191
193
|
return;
|
|
192
194
|
}
|
|
193
195
|
void deleteSession(deleteProjectSlug, deleteMsg.sessionId).then(function () {
|
|
194
|
-
|
|
196
|
+
invalidateSessionCache(deleteProjectSlug);
|
|
197
|
+
void listSessions(deleteProjectSlug, { limit: 40 }).then(function (result) {
|
|
195
198
|
sendTo(clientId, {
|
|
196
199
|
type: "session:list",
|
|
197
200
|
projectSlug: deleteProjectSlug,
|
|
198
|
-
sessions,
|
|
201
|
+
sessions: result.sessions,
|
|
202
|
+
totalCount: result.totalCount,
|
|
199
203
|
});
|
|
200
204
|
});
|
|
201
205
|
});
|
|
@@ -11,7 +11,7 @@ import { sendTo, broadcast } from "../ws/broadcast";
|
|
|
11
11
|
import { syncSessionToPeers } from "../mesh/session-sync";
|
|
12
12
|
import { resolveSkillContent } from "../handlers/skills";
|
|
13
13
|
import { getPluginMcpServers } from "../handlers/plugins";
|
|
14
|
-
import { guessContextWindow, getSessionTitle, renameSession, listSessions } from "./session";
|
|
14
|
+
import { guessContextWindow, getSessionTitle, renameSession, listSessions, invalidateSessionCache } from "./session";
|
|
15
15
|
import { getLatticeHome, loadConfig } from "../config";
|
|
16
16
|
import { log } from "../logger";
|
|
17
17
|
import { getDailySpend, invalidateDailySpendCache } from "../analytics/engine";
|
|
@@ -1032,8 +1032,9 @@ export function startChatStream(options: ChatStreamOptions): void {
|
|
|
1032
1032
|
void renameSession(projectSlug, sessionId, newTitle).then(function (ok) {
|
|
1033
1033
|
if (!ok) return;
|
|
1034
1034
|
log.session("Auto-titled session %s: %s", sessionId, newTitle);
|
|
1035
|
-
|
|
1036
|
-
|
|
1035
|
+
invalidateSessionCache(projectSlug);
|
|
1036
|
+
void listSessions(projectSlug, { limit: 40 }).then(function (result) {
|
|
1037
|
+
broadcast({ type: "session:list", projectSlug, sessions: result.sessions, totalCount: result.totalCount });
|
|
1037
1038
|
});
|
|
1038
1039
|
});
|
|
1039
1040
|
});
|
|
@@ -404,10 +404,21 @@ export async function getSessionPreview(projectSlug: string, sessionId: string):
|
|
|
404
404
|
}
|
|
405
405
|
}
|
|
406
406
|
|
|
407
|
-
|
|
407
|
+
var sessionListCache = new Map<string, { sessions: SessionSummary[]; time: number }>();
|
|
408
|
+
var SESSION_CACHE_TTL = 5000;
|
|
409
|
+
|
|
410
|
+
export async function listSessions(projectSlug: string, options?: { offset?: number; limit?: number; noCache?: boolean }): Promise<{ sessions: SessionSummary[]; totalCount: number }> {
|
|
408
411
|
var projectPath = getProjectPath(projectSlug);
|
|
409
412
|
if (!projectPath) {
|
|
410
|
-
return [];
|
|
413
|
+
return { sessions: [], totalCount: 0 };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
var cached = sessionListCache.get(projectSlug);
|
|
417
|
+
if (cached && !options?.noCache && Date.now() - cached.time < SESSION_CACHE_TTL) {
|
|
418
|
+
var offset = options?.offset ?? 0;
|
|
419
|
+
var limit = options?.limit ?? 0;
|
|
420
|
+
var sliced = limit > 0 ? cached.sessions.slice(offset, offset + limit) : cached.sessions;
|
|
421
|
+
return { sessions: sliced, totalCount: cached.sessions.length };
|
|
411
422
|
}
|
|
412
423
|
|
|
413
424
|
try {
|
|
@@ -416,13 +427,22 @@ export async function listSessions(projectSlug: string): Promise<SessionSummary[
|
|
|
416
427
|
return mapSDKSession(s, projectSlug);
|
|
417
428
|
});
|
|
418
429
|
summaries.sort(function (a, b) { return b.updatedAt - a.updatedAt; });
|
|
419
|
-
|
|
430
|
+
sessionListCache.set(projectSlug, { sessions: summaries, time: Date.now() });
|
|
431
|
+
|
|
432
|
+
var offset2 = options?.offset ?? 0;
|
|
433
|
+
var limit2 = options?.limit ?? 0;
|
|
434
|
+
var sliced2 = limit2 > 0 ? summaries.slice(offset2, offset2 + limit2) : summaries;
|
|
435
|
+
return { sessions: sliced2, totalCount: summaries.length };
|
|
420
436
|
} catch (err) {
|
|
421
437
|
log.session("Failed to list SDK sessions: %O", err);
|
|
422
|
-
return [];
|
|
438
|
+
return { sessions: [], totalCount: 0 };
|
|
423
439
|
}
|
|
424
440
|
}
|
|
425
441
|
|
|
442
|
+
export function invalidateSessionCache(projectSlug: string): void {
|
|
443
|
+
sessionListCache.delete(projectSlug);
|
|
444
|
+
}
|
|
445
|
+
|
|
426
446
|
export async function getSessionTitle(projectSlug: string, sessionId: string): Promise<string> {
|
|
427
447
|
var projectPath = getProjectPath(projectSlug);
|
|
428
448
|
var options = projectPath ? { dir: projectPath } : undefined;
|