@cryptiklemur/lattice 1.46.6 → 1.47.0
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/components/chat/ChatView.tsx +7 -72
- package/client/src/components/chat/ElicitationCard.tsx +235 -0
- package/client/src/components/chat/Message.tsx +16 -0
- package/client/src/components/settings/BudgetSettings.tsx +6 -2
- package/client/src/components/sidebar/UserIsland.tsx +141 -14
- package/client/src/components/ui/SaveFooter.tsx +28 -3
- package/client/src/hooks/useSession.ts +89 -42
- package/client/src/hooks/useWebSocket.ts +1 -1
- package/client/src/stores/session.ts +64 -16
- package/package.json +1 -1
- package/server/src/daemon.ts +25 -8
- package/server/src/handlers/chat.ts +12 -1
- package/server/src/handlers/session.ts +1 -15
- package/server/src/handlers/settings.ts +3 -0
- package/server/src/index.ts +3 -0
- package/server/src/project/sdk-bridge.ts +119 -166
- package/server/src/project/session.ts +36 -7
- package/server/src/project/warmup.ts +232 -0
- package/shared/src/messages.ts +49 -11
- package/shared/src/models.ts +7 -1
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { Loader2, Check, X } from "lucide-react";
|
|
1
3
|
import type { SaveState } from "../../hooks/useSaveState";
|
|
4
|
+
import { showToast } from "./Toast";
|
|
2
5
|
|
|
3
6
|
interface SaveFooterProps {
|
|
4
7
|
dirty: boolean;
|
|
@@ -10,6 +13,18 @@ interface SaveFooterProps {
|
|
|
10
13
|
|
|
11
14
|
export function SaveFooter({ dirty, saving, saveState, onSave, extraStatus }: SaveFooterProps) {
|
|
12
15
|
var disabled = saving || (!dirty && saveState !== "error");
|
|
16
|
+
var prevStateRef = useRef<SaveState>("idle");
|
|
17
|
+
|
|
18
|
+
useEffect(function () {
|
|
19
|
+
if (prevStateRef.current !== saveState) {
|
|
20
|
+
if (saveState === "saved") {
|
|
21
|
+
showToast("Settings saved", "success");
|
|
22
|
+
} else if (saveState === "error") {
|
|
23
|
+
showToast("Failed to save settings — server did not respond within 5 seconds", "error");
|
|
24
|
+
}
|
|
25
|
+
prevStateRef.current = saveState;
|
|
26
|
+
}
|
|
27
|
+
}, [saveState]);
|
|
13
28
|
|
|
14
29
|
return (
|
|
15
30
|
<div className="flex items-center justify-end gap-3">
|
|
@@ -20,18 +35,28 @@ export function SaveFooter({ dirty, saving, saveState, onSave, extraStatus }: Sa
|
|
|
20
35
|
<div className="text-[11px] text-warning/70">Unsaved changes</div>
|
|
21
36
|
)}
|
|
22
37
|
{saveState === "error" && (
|
|
23
|
-
<div className="text-[11px] text-error">
|
|
38
|
+
<div className="flex items-center gap-1.5 text-[11px] text-error">
|
|
39
|
+
<X size={11} />
|
|
40
|
+
Save failed — server did not respond
|
|
41
|
+
</div>
|
|
42
|
+
)}
|
|
43
|
+
{saveState === "saved" && (
|
|
44
|
+
<div className="flex items-center gap-1.5 text-[11px] text-success/70">
|
|
45
|
+
<Check size={11} />
|
|
46
|
+
Saved
|
|
47
|
+
</div>
|
|
24
48
|
)}
|
|
25
49
|
<button
|
|
26
50
|
onClick={onSave}
|
|
27
51
|
disabled={disabled}
|
|
28
52
|
className={
|
|
29
|
-
"btn btn-sm " +
|
|
53
|
+
"btn btn-sm gap-1.5 " +
|
|
30
54
|
(saveState === "saved" ? "btn-success" : saveState === "error" ? "btn-error" : "btn-primary") +
|
|
31
55
|
(disabled ? " opacity-50 cursor-not-allowed" : "")
|
|
32
56
|
}
|
|
33
57
|
>
|
|
34
|
-
{saving
|
|
58
|
+
{saving && <Loader2 size={13} className="animate-spin" />}
|
|
59
|
+
{saving ? "Saving..." : saveState === "error" ? "Retry" : "Save Changes"}
|
|
35
60
|
</button>
|
|
36
61
|
</div>
|
|
37
62
|
);
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
ChatContextUsageMessage,
|
|
14
14
|
ChatContextBreakdownMessage,
|
|
15
15
|
ChatPromptSuggestionMessage,
|
|
16
|
+
ChatElicitationRequestMessage,
|
|
16
17
|
SessionHistoryMessage,
|
|
17
18
|
ServerMessage,
|
|
18
19
|
} from "@lattice/shared";
|
|
@@ -49,12 +50,12 @@ import {
|
|
|
49
50
|
removeQueuedMessage,
|
|
50
51
|
updateQueuedMessage,
|
|
51
52
|
clearMessageQueue,
|
|
52
|
-
setSessionBusy,
|
|
53
53
|
addPromptQuestion,
|
|
54
54
|
addTodoUpdate,
|
|
55
55
|
setIsPlanMode,
|
|
56
56
|
setBudgetStatus,
|
|
57
57
|
setBudgetExceeded,
|
|
58
|
+
updateRateLimit,
|
|
58
59
|
} from "../stores/session";
|
|
59
60
|
import type { SessionState, BudgetStatus } from "../stores/session";
|
|
60
61
|
|
|
@@ -119,7 +120,12 @@ export function useSession(): UseSessionReturn {
|
|
|
119
120
|
setFailedInput(null);
|
|
120
121
|
setPromptSuggestion(null);
|
|
121
122
|
setIsProcessing(true);
|
|
122
|
-
|
|
123
|
+
addSessionMessage({
|
|
124
|
+
type: "user",
|
|
125
|
+
uuid: "optimistic-" + Date.now(),
|
|
126
|
+
text: text,
|
|
127
|
+
timestamp: Date.now(),
|
|
128
|
+
} as HistoryMessage);
|
|
123
129
|
pinTab("chat-" + currentSessionId);
|
|
124
130
|
sendRef.current(msg as ChatSendMessage);
|
|
125
131
|
}
|
|
@@ -143,6 +149,16 @@ export function useSession(): UseSessionReturn {
|
|
|
143
149
|
if (isStaleStream()) return;
|
|
144
150
|
var m = msg as ChatUserMessage;
|
|
145
151
|
setCurrentAssistantUuid(null);
|
|
152
|
+
var messages = getSessionStore().state.messages;
|
|
153
|
+
var last = messages.length > 0 ? messages[messages.length - 1] : null;
|
|
154
|
+
if (last && last.type === "user" && last.uuid && last.uuid.startsWith("optimistic-") && last.text === m.text) {
|
|
155
|
+
getSessionStore().setState(function (s) {
|
|
156
|
+
var updated = s.messages.slice();
|
|
157
|
+
updated[updated.length - 1] = { ...updated[updated.length - 1], uuid: m.uuid };
|
|
158
|
+
return { ...s, messages: updated };
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
146
162
|
addSessionMessage({
|
|
147
163
|
type: "user",
|
|
148
164
|
uuid: m.uuid,
|
|
@@ -279,6 +295,22 @@ export function useSession(): UseSessionReturn {
|
|
|
279
295
|
updatePermissionStatus(m.requestId, m.status);
|
|
280
296
|
}
|
|
281
297
|
|
|
298
|
+
function handleElicitationRequest(msg: ServerMessage) {
|
|
299
|
+
var m = msg as ChatElicitationRequestMessage;
|
|
300
|
+
setCurrentAssistantUuid(null);
|
|
301
|
+
addSessionMessage({
|
|
302
|
+
type: "elicitation",
|
|
303
|
+
toolId: m.requestId,
|
|
304
|
+
elicitationMode: m.mode,
|
|
305
|
+
elicitationServerName: m.serverName,
|
|
306
|
+
elicitationMessage: m.message,
|
|
307
|
+
elicitationUrl: m.url,
|
|
308
|
+
elicitationSchema: m.requestedSchema,
|
|
309
|
+
elicitationStatus: "pending",
|
|
310
|
+
timestamp: Date.now(),
|
|
311
|
+
} as HistoryMessage);
|
|
312
|
+
}
|
|
313
|
+
|
|
282
314
|
function handleHistoryPage(msg: ServerMessage) {
|
|
283
315
|
var m = msg as { type: string; sessionId: string; messages: HistoryMessage[]; hasMore: boolean };
|
|
284
316
|
var state = getSessionStore().state;
|
|
@@ -306,34 +338,46 @@ export function useSession(): UseSessionReturn {
|
|
|
306
338
|
var projectSlug = m.projectSlug || getSessionStore().state.activeProjectSlug;
|
|
307
339
|
setSidebarSessionId(m.sessionId);
|
|
308
340
|
streamSessionId = m.sessionId;
|
|
309
|
-
if (m.busy) {
|
|
310
|
-
activeStreamGeneration = getStreamGeneration();
|
|
311
|
-
}
|
|
312
341
|
if (m.title) {
|
|
313
342
|
updateSessionTabTitle(m.sessionId, m.title);
|
|
314
343
|
}
|
|
315
|
-
getSessionStore().
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
344
|
+
var currentState = getSessionStore().state;
|
|
345
|
+
var alreadyCached = currentState.activeSessionId === m.sessionId
|
|
346
|
+
&& !currentState.historyLoading
|
|
347
|
+
&& currentState.messages.length > 0;
|
|
348
|
+
|
|
349
|
+
if (alreadyCached) {
|
|
350
|
+
getSessionStore().setState(function (state) {
|
|
351
|
+
return {
|
|
352
|
+
...state,
|
|
353
|
+
activeSessionTitle: m.title ?? state.activeSessionTitle,
|
|
354
|
+
historyHasMore: m.hasMore || state.historyHasMore,
|
|
355
|
+
historyTotalMessages: m.totalMessages || state.historyTotalMessages,
|
|
356
|
+
wasInterrupted: m.interrupted || false,
|
|
357
|
+
};
|
|
358
|
+
});
|
|
359
|
+
} else {
|
|
360
|
+
getSessionStore().setState(function (state) {
|
|
361
|
+
return {
|
|
362
|
+
...state,
|
|
363
|
+
activeProjectSlug: projectSlug,
|
|
364
|
+
activeSessionId: m.sessionId,
|
|
365
|
+
activeSessionTitle: m.title ?? null,
|
|
366
|
+
messages: mergeToolResults(m.messages),
|
|
367
|
+
isProcessing: false,
|
|
368
|
+
currentStatus: null,
|
|
369
|
+
pendingPermissionCount: 0,
|
|
370
|
+
lastResponseCost: null,
|
|
371
|
+
lastResponseDuration: null,
|
|
372
|
+
lastReadIndex: null,
|
|
373
|
+
historyLoading: false,
|
|
374
|
+
historyHasMore: m.hasMore || false,
|
|
375
|
+
historyTotalMessages: m.totalMessages || m.messages.length,
|
|
376
|
+
wasInterrupted: m.interrupted || false,
|
|
377
|
+
isPlanMode: false,
|
|
378
|
+
};
|
|
379
|
+
});
|
|
380
|
+
}
|
|
337
381
|
var storedIndex = getLastReadIndex(m.sessionId);
|
|
338
382
|
if (storedIndex >= 0 && storedIndex < m.messages.length) {
|
|
339
383
|
setLastReadIndex(storedIndex);
|
|
@@ -360,17 +404,6 @@ export function useSession(): UseSessionReturn {
|
|
|
360
404
|
setPromptSuggestion(m.suggestion);
|
|
361
405
|
}
|
|
362
406
|
|
|
363
|
-
function handleSessionBusy(msg: ServerMessage) {
|
|
364
|
-
var m = msg as { type: string; sessionId: string; busy: boolean; busyOwner?: "cli" | "lattice" };
|
|
365
|
-
var sessionState = getSessionStore().state;
|
|
366
|
-
if (m.sessionId === sessionState.activeSessionId) {
|
|
367
|
-
if (m.busy && sessionState.isProcessing) {
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
setSessionBusy(m.busy, m.busyOwner);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
407
|
function handlePromptRequest(msg: ServerMessage) {
|
|
375
408
|
if (isStaleStream()) return;
|
|
376
409
|
var m = msg as { type: string; requestId: string; questions: Array<{ question: string; header: string; options: Array<{ label: string; description: string; preview?: string }>; multiSelect: boolean }> };
|
|
@@ -397,6 +430,20 @@ export function useSession(): UseSessionReturn {
|
|
|
397
430
|
setBudgetStatus({ dailySpend: m.dailySpend, dailyLimit: m.dailyLimit, enforcement: m.enforcement });
|
|
398
431
|
}
|
|
399
432
|
|
|
433
|
+
function handleRateLimit(msg: ServerMessage) {
|
|
434
|
+
var m = msg as { type: string; status: "allowed" | "allowed_warning" | "rejected"; utilization?: number; resetsAt?: number; rateLimitType?: string; overageStatus?: string; overageResetsAt?: number; isUsingOverage?: boolean };
|
|
435
|
+
updateRateLimit({
|
|
436
|
+
status: m.status,
|
|
437
|
+
utilization: m.utilization,
|
|
438
|
+
resetsAt: m.resetsAt,
|
|
439
|
+
rateLimitType: m.rateLimitType || "unknown",
|
|
440
|
+
overageStatus: m.overageStatus,
|
|
441
|
+
overageResetsAt: m.overageResetsAt,
|
|
442
|
+
isUsingOverage: m.isUsingOverage,
|
|
443
|
+
updatedAt: Date.now(),
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
400
447
|
function handleBudgetExceeded(msg: ServerMessage) {
|
|
401
448
|
setBudgetExceeded(true);
|
|
402
449
|
setIsProcessing(false);
|
|
@@ -417,13 +464,14 @@ export function useSession(): UseSessionReturn {
|
|
|
417
464
|
subscribe("session:history", handleHistory);
|
|
418
465
|
subscribe("session:history_page_result", handleHistoryPage);
|
|
419
466
|
subscribe("chat:prompt_suggestion", handlePromptSuggestion);
|
|
420
|
-
subscribe("session:busy", handleSessionBusy);
|
|
421
467
|
subscribe("chat:prompt_request", handlePromptRequest);
|
|
422
468
|
subscribe("chat:prompt_resolved", handlePromptResolved);
|
|
423
469
|
subscribe("chat:todo_update", handleTodoUpdate);
|
|
424
470
|
subscribe("chat:plan_mode", handlePlanMode);
|
|
425
471
|
subscribe("budget:status", handleBudgetStatus);
|
|
426
472
|
subscribe("budget:exceeded", handleBudgetExceeded);
|
|
473
|
+
subscribe("chat:elicitation_request", handleElicitationRequest);
|
|
474
|
+
subscribe("chat:rate_limit", handleRateLimit);
|
|
427
475
|
|
|
428
476
|
return function () {
|
|
429
477
|
subscriptionsActive--;
|
|
@@ -441,13 +489,14 @@ export function useSession(): UseSessionReturn {
|
|
|
441
489
|
unsubscribe("session:history", handleHistory);
|
|
442
490
|
unsubscribe("session:history_page_result", handleHistoryPage);
|
|
443
491
|
unsubscribe("chat:prompt_suggestion", handlePromptSuggestion);
|
|
444
|
-
unsubscribe("session:busy", handleSessionBusy);
|
|
445
492
|
unsubscribe("chat:prompt_request", handlePromptRequest);
|
|
446
493
|
unsubscribe("chat:prompt_resolved", handlePromptResolved);
|
|
447
494
|
unsubscribe("chat:todo_update", handleTodoUpdate);
|
|
448
495
|
unsubscribe("chat:plan_mode", handlePlanMode);
|
|
449
496
|
unsubscribe("budget:status", handleBudgetStatus);
|
|
450
497
|
unsubscribe("budget:exceeded", handleBudgetExceeded);
|
|
498
|
+
unsubscribe("chat:elicitation_request", handleElicitationRequest);
|
|
499
|
+
unsubscribe("chat:rate_limit", handleRateLimit);
|
|
451
500
|
};
|
|
452
501
|
}, [subscribe, unsubscribe]);
|
|
453
502
|
|
|
@@ -481,8 +530,6 @@ export function useSession(): UseSessionReturn {
|
|
|
481
530
|
promptSuggestion: state.promptSuggestion,
|
|
482
531
|
failedInput: state.failedInput,
|
|
483
532
|
messageQueue: state.messageQueue,
|
|
484
|
-
isBusy: state.isBusy,
|
|
485
|
-
busyOwner: state.busyOwner,
|
|
486
533
|
isPlanMode: state.isPlanMode,
|
|
487
534
|
pendingPrefill: state.pendingPrefill,
|
|
488
535
|
budgetStatus: state.budgetStatus,
|
|
@@ -23,7 +23,7 @@ export function useWebSocket(): WebSocketContextValue {
|
|
|
23
23
|
|
|
24
24
|
export function getWebSocketUrl(): string {
|
|
25
25
|
if (import.meta.env.DEV) {
|
|
26
|
-
return "ws://" + window.location.hostname + ":
|
|
26
|
+
return "ws://" + window.location.hostname + ":17654/ws";
|
|
27
27
|
}
|
|
28
28
|
var protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
29
29
|
return protocol + "//" + window.location.host + "/ws";
|
|
@@ -28,6 +28,17 @@ export interface BudgetStatus {
|
|
|
28
28
|
enforcement: "warning" | "soft-block" | "hard-block";
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
export interface RateLimitEntry {
|
|
32
|
+
status: "allowed" | "allowed_warning" | "rejected";
|
|
33
|
+
utilization?: number;
|
|
34
|
+
resetsAt?: number;
|
|
35
|
+
rateLimitType: string;
|
|
36
|
+
overageStatus?: string;
|
|
37
|
+
overageResetsAt?: number;
|
|
38
|
+
isUsingOverage?: boolean;
|
|
39
|
+
updatedAt: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
31
42
|
export interface SessionState {
|
|
32
43
|
messages: HistoryMessage[];
|
|
33
44
|
isProcessing: boolean;
|
|
@@ -48,12 +59,11 @@ export interface SessionState {
|
|
|
48
59
|
promptSuggestion: string | null;
|
|
49
60
|
failedInput: string | null;
|
|
50
61
|
messageQueue: string[];
|
|
51
|
-
isBusy: boolean;
|
|
52
|
-
busyOwner: "cli" | "lattice" | null;
|
|
53
62
|
isPlanMode: boolean;
|
|
54
63
|
pendingPrefill: string | null;
|
|
55
64
|
budgetStatus: BudgetStatus | null;
|
|
56
65
|
budgetExceeded: boolean;
|
|
66
|
+
rateLimits: Record<string, RateLimitEntry>;
|
|
57
67
|
}
|
|
58
68
|
|
|
59
69
|
var sessionStore = new Store<SessionState>({
|
|
@@ -76,12 +86,11 @@ var sessionStore = new Store<SessionState>({
|
|
|
76
86
|
promptSuggestion: null,
|
|
77
87
|
failedInput: null,
|
|
78
88
|
messageQueue: [],
|
|
79
|
-
isBusy: false,
|
|
80
|
-
busyOwner: null,
|
|
81
89
|
isPlanMode: false,
|
|
82
90
|
pendingPrefill: null,
|
|
83
91
|
budgetStatus: null,
|
|
84
92
|
budgetExceeded: false,
|
|
93
|
+
rateLimits: {},
|
|
85
94
|
});
|
|
86
95
|
|
|
87
96
|
var streamGeneration = 0;
|
|
@@ -212,20 +221,47 @@ export function setIsProcessing(processing: boolean): void {
|
|
|
212
221
|
});
|
|
213
222
|
}
|
|
214
223
|
|
|
224
|
+
var sessionMessageCache = new Map<string, { messages: HistoryMessage[]; title: string | null; hasMore: boolean; totalMessages: number }>();
|
|
225
|
+
var MAX_CACHED_SESSIONS = 10;
|
|
226
|
+
|
|
227
|
+
export function cacheCurrentSession(): void {
|
|
228
|
+
var state = sessionStore.state;
|
|
229
|
+
if (state.activeSessionId && state.messages.length > 0) {
|
|
230
|
+
sessionMessageCache.set(state.activeSessionId, {
|
|
231
|
+
messages: state.messages,
|
|
232
|
+
title: state.activeSessionTitle,
|
|
233
|
+
hasMore: state.historyHasMore,
|
|
234
|
+
totalMessages: state.historyTotalMessages,
|
|
235
|
+
});
|
|
236
|
+
if (sessionMessageCache.size > MAX_CACHED_SESSIONS) {
|
|
237
|
+
var firstKey = sessionMessageCache.keys().next().value;
|
|
238
|
+
if (firstKey) sessionMessageCache.delete(firstKey);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function getCachedSession(sessionId: string): { messages: HistoryMessage[]; title: string | null; hasMore: boolean; totalMessages: number } | undefined {
|
|
244
|
+
return sessionMessageCache.get(sessionId);
|
|
245
|
+
}
|
|
246
|
+
|
|
215
247
|
export function setActiveSession(projectSlug: string | null, sessionId: string | null, title?: string | null): void {
|
|
216
248
|
var prevSessionId = sessionStore.state.activeSessionId;
|
|
217
249
|
if (prevSessionId) {
|
|
218
250
|
markSessionRead(prevSessionId, sessionStore.state.messages.length);
|
|
251
|
+
cacheCurrentSession();
|
|
219
252
|
}
|
|
220
253
|
currentAssistantUuid = null;
|
|
221
254
|
incrementStreamGeneration();
|
|
255
|
+
|
|
256
|
+
var cached = sessionId ? sessionMessageCache.get(sessionId) : undefined;
|
|
257
|
+
|
|
222
258
|
sessionStore.setState(function (state) {
|
|
223
259
|
return {
|
|
224
260
|
...state,
|
|
225
261
|
activeProjectSlug: projectSlug,
|
|
226
262
|
activeSessionId: sessionId,
|
|
227
|
-
activeSessionTitle: title ?? null,
|
|
228
|
-
messages: [],
|
|
263
|
+
activeSessionTitle: cached?.title ?? title ?? null,
|
|
264
|
+
messages: cached?.messages ?? [],
|
|
229
265
|
isProcessing: false,
|
|
230
266
|
currentStatus: null,
|
|
231
267
|
contextUsage: null,
|
|
@@ -234,12 +270,13 @@ export function setActiveSession(projectSlug: string | null, sessionId: string |
|
|
|
234
270
|
lastResponseCost: null,
|
|
235
271
|
lastResponseDuration: null,
|
|
236
272
|
lastReadIndex: null,
|
|
237
|
-
historyLoading:
|
|
273
|
+
historyLoading: !cached,
|
|
274
|
+
historyHasMore: cached?.hasMore ?? false,
|
|
275
|
+
historyTotalMessages: cached?.totalMessages ?? 0,
|
|
238
276
|
wasInterrupted: false,
|
|
239
277
|
promptSuggestion: null,
|
|
240
278
|
failedInput: null,
|
|
241
279
|
messageQueue: [],
|
|
242
|
-
isBusy: false,
|
|
243
280
|
isPlanMode: false,
|
|
244
281
|
pendingPrefill: state.pendingPrefill,
|
|
245
282
|
};
|
|
@@ -303,8 +340,6 @@ export function clearSession(): void {
|
|
|
303
340
|
promptSuggestion: null,
|
|
304
341
|
failedInput: null,
|
|
305
342
|
messageQueue: [],
|
|
306
|
-
isBusy: false,
|
|
307
|
-
busyOwner: null,
|
|
308
343
|
isPlanMode: false,
|
|
309
344
|
pendingPrefill: null,
|
|
310
345
|
budgetStatus: null,
|
|
@@ -337,12 +372,6 @@ export function setFailedInput(text: string | null): void {
|
|
|
337
372
|
});
|
|
338
373
|
}
|
|
339
374
|
|
|
340
|
-
export function setSessionBusy(busy: boolean, owner?: "cli" | "lattice" | null): void {
|
|
341
|
-
sessionStore.setState(function (state) {
|
|
342
|
-
return { ...state, isBusy: busy, busyOwner: busy ? (owner ?? null) : null };
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
|
|
346
375
|
export function setIsPlanMode(active: boolean): void {
|
|
347
376
|
sessionStore.setState(function (state) {
|
|
348
377
|
return { ...state, isPlanMode: active };
|
|
@@ -355,6 +384,25 @@ export function setBudgetStatus(status: BudgetStatus | null): void {
|
|
|
355
384
|
});
|
|
356
385
|
}
|
|
357
386
|
|
|
387
|
+
export function updateRateLimit(entry: RateLimitEntry): void {
|
|
388
|
+
sessionStore.setState(function (state) {
|
|
389
|
+
var updated = { ...state.rateLimits };
|
|
390
|
+
updated[entry.rateLimitType] = entry;
|
|
391
|
+
try { localStorage.setItem("lattice:rateLimits", JSON.stringify(updated)); } catch {}
|
|
392
|
+
return { ...state, rateLimits: updated };
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export function loadCachedRateLimits(): void {
|
|
397
|
+
try {
|
|
398
|
+
var raw = localStorage.getItem("lattice:rateLimits");
|
|
399
|
+
if (raw) {
|
|
400
|
+
var parsed = JSON.parse(raw) as Record<string, RateLimitEntry>;
|
|
401
|
+
sessionStore.setState(function (state) { return { ...state, rateLimits: parsed }; });
|
|
402
|
+
}
|
|
403
|
+
} catch {}
|
|
404
|
+
}
|
|
405
|
+
|
|
358
406
|
export function setBudgetExceeded(exceeded: boolean): void {
|
|
359
407
|
sessionStore.setState(function (state) {
|
|
360
408
|
return { ...state, budgetExceeded: exceeded };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cryptiklemur/lattice",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.47.0",
|
|
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>",
|
package/server/src/daemon.ts
CHANGED
|
@@ -18,8 +18,9 @@ import { detectIdeProjectName } from "./handlers/settings";
|
|
|
18
18
|
import "./handlers/session";
|
|
19
19
|
import "./handlers/chat";
|
|
20
20
|
import "./handlers/attachment";
|
|
21
|
-
import { loadInterruptedSessions,
|
|
22
|
-
import {
|
|
21
|
+
import { loadInterruptedSessions, cleanupClientPermissions, cleanupClientElicitations, getActiveStreamCountForProject } from "./project/sdk-bridge";
|
|
22
|
+
import { runWarmup, isWarmupComplete, getWarmupModels, getWarmupAccountInfo } from "./project/warmup";
|
|
23
|
+
import { clearActiveSession } from "./handlers/chat";
|
|
23
24
|
import { clearActiveProject } from "./handlers/fs";
|
|
24
25
|
import { clearClientRemoteNode } from "./ws/router";
|
|
25
26
|
import "./handlers/fs";
|
|
@@ -324,13 +325,27 @@ export async function startDaemon(portOverride?: number | null): Promise<void> {
|
|
|
324
325
|
var connectConfig = loadConfig();
|
|
325
326
|
var connectIdentity = loadOrCreateIdentity();
|
|
326
327
|
var localProjects = connectConfig.projects.map(function (p: typeof connectConfig.projects[number]) {
|
|
327
|
-
return { slug: p.slug, path: p.path, title: p.title, nodeId: connectIdentity.id, nodeName: connectConfig.name, isRemote: false, ideProjectName: detectIdeProjectName(p.path), activeSessions:
|
|
328
|
+
return { slug: p.slug, path: p.path, title: p.title, nodeId: connectIdentity.id, nodeName: connectConfig.name, isRemote: false, ideProjectName: detectIdeProjectName(p.path), activeSessions: getActiveStreamCountForProject(p.slug) };
|
|
328
329
|
});
|
|
329
330
|
var connectRemoteProjects = getAllRemoteProjects(connectIdentity.id);
|
|
330
331
|
sendTo(ws.data.id, {
|
|
331
332
|
type: "projects:list",
|
|
332
333
|
projects: localProjects.concat(connectRemoteProjects as unknown as typeof localProjects),
|
|
333
334
|
});
|
|
335
|
+
if (isWarmupComplete()) {
|
|
336
|
+
sendTo(ws.data.id, { type: "warmup:models", models: getWarmupModels() } as any);
|
|
337
|
+
var accountInfo = getWarmupAccountInfo();
|
|
338
|
+
if (accountInfo) {
|
|
339
|
+
sendTo(ws.data.id, {
|
|
340
|
+
type: "warmup:account",
|
|
341
|
+
email: accountInfo.email,
|
|
342
|
+
organization: accountInfo.organization,
|
|
343
|
+
subscriptionType: accountInfo.subscriptionType,
|
|
344
|
+
apiKeySource: accountInfo.apiKeySource,
|
|
345
|
+
apiProvider: accountInfo.apiProvider,
|
|
346
|
+
} as any);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
334
349
|
},
|
|
335
350
|
message(ws: ServerWebSocket<WsData>, message: string | Buffer) {
|
|
336
351
|
var now = Date.now();
|
|
@@ -354,10 +369,6 @@ export async function startDaemon(portOverride?: number | null): Promise<void> {
|
|
|
354
369
|
}
|
|
355
370
|
},
|
|
356
371
|
close(ws: ServerWebSocket<WsData>) {
|
|
357
|
-
var activeSession = getActiveSession(ws.data.id);
|
|
358
|
-
if (activeSession) {
|
|
359
|
-
unwatchSessionLock(activeSession.sessionId);
|
|
360
|
-
}
|
|
361
372
|
clearActiveSession(ws.data.id);
|
|
362
373
|
clearActiveProject(ws.data.id);
|
|
363
374
|
clearClientRemoteNode(ws.data.id);
|
|
@@ -365,6 +376,7 @@ export async function startDaemon(portOverride?: number | null): Promise<void> {
|
|
|
365
376
|
cleanupClientTerminals(ws.data.id);
|
|
366
377
|
cleanupClientAttachments(ws.data.id);
|
|
367
378
|
cleanupClientPermissions(ws.data.id);
|
|
379
|
+
cleanupClientElicitations(ws.data.id);
|
|
368
380
|
clientRateLimits.delete(ws.data.id);
|
|
369
381
|
log.ws("Client disconnected: %s", ws.data.id);
|
|
370
382
|
},
|
|
@@ -396,6 +408,11 @@ export async function startDaemon(portOverride?: number | null): Promise<void> {
|
|
|
396
408
|
|
|
397
409
|
loadInterruptedSessions();
|
|
398
410
|
|
|
411
|
+
var firstProject = config.projects[0];
|
|
412
|
+
if (firstProject) {
|
|
413
|
+
void runWarmup(firstProject.path);
|
|
414
|
+
}
|
|
415
|
+
|
|
399
416
|
onPeerConnected(function (nodeId: string) {
|
|
400
417
|
broadcast({ type: "mesh:node_online", nodeId: nodeId });
|
|
401
418
|
});
|
|
@@ -420,7 +437,7 @@ export async function startDaemon(portOverride?: number | null): Promise<void> {
|
|
|
420
437
|
var currentIdentity = loadOrCreateIdentity();
|
|
421
438
|
broadcast({ type: "mesh:nodes", nodes: buildNodesMessage() });
|
|
422
439
|
var localProjects = currentConfig.projects.map(function (p: typeof currentConfig.projects[number]) {
|
|
423
|
-
return { slug: p.slug, path: p.path, title: p.title, nodeId: currentIdentity.id, nodeName: currentConfig.name, isRemote: false, ideProjectName: detectIdeProjectName(p.path), activeSessions:
|
|
440
|
+
return { slug: p.slug, path: p.path, title: p.title, nodeId: currentIdentity.id, nodeName: currentConfig.name, isRemote: false, ideProjectName: detectIdeProjectName(p.path), activeSessions: getActiveStreamCountForProject(p.slug) };
|
|
424
441
|
});
|
|
425
442
|
var remoteProjects = getAllRemoteProjects(currentIdentity.id);
|
|
426
443
|
broadcast({
|
|
@@ -3,7 +3,7 @@ import { registerHandler } from "../ws/router";
|
|
|
3
3
|
import { sendTo } from "../ws/broadcast";
|
|
4
4
|
import { getProjectBySlug } from "../project/registry";
|
|
5
5
|
import { loadConfig } from "../config";
|
|
6
|
-
import { startChatStream, getPendingPermission, deletePendingPermission, addAutoApprovedTool, setSessionPermissionOverride, getActiveStream, buildPermissionRule } from "../project/sdk-bridge";
|
|
6
|
+
import { startChatStream, getPendingPermission, deletePendingPermission, addAutoApprovedTool, setSessionPermissionOverride, getActiveStream, buildPermissionRule, getPendingElicitation, resolveElicitation } from "../project/sdk-bridge";
|
|
7
7
|
import { getAttachments } from "./attachment";
|
|
8
8
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
@@ -281,6 +281,17 @@ registerHandler("chat", function (clientId: string, message: ClientMessage) {
|
|
|
281
281
|
return;
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
if (message.type === "chat:elicitation_response") {
|
|
285
|
+
var elicitMsg = message as { type: string; requestId: string; action: "accept" | "decline"; content?: Record<string, unknown> };
|
|
286
|
+
var pendingElicit = getPendingElicitation(elicitMsg.requestId);
|
|
287
|
+
if (!pendingElicit) return;
|
|
288
|
+
resolveElicitation(elicitMsg.requestId, {
|
|
289
|
+
action: elicitMsg.action,
|
|
290
|
+
content: elicitMsg.content || {},
|
|
291
|
+
});
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
284
295
|
if (message.type === "chat:set_permission_mode") {
|
|
285
296
|
var modeMsg = message as ChatSetPermissionModeMessage;
|
|
286
297
|
var activeSession = activeSessionByClient.get(clientId);
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
import { getContextBreakdown } from "../project/context-breakdown";
|
|
28
28
|
import { setActiveSession, getActiveSession } from "./chat";
|
|
29
29
|
import { setActiveProject } from "./fs";
|
|
30
|
-
import { wasSessionInterrupted, clearInterruptedFlag
|
|
30
|
+
import { wasSessionInterrupted, clearInterruptedFlag } from "../project/sdk-bridge";
|
|
31
31
|
import { log } from "../logger";
|
|
32
32
|
|
|
33
33
|
registerHandler("session", function (clientId: string, message: ClientMessage) {
|
|
@@ -105,7 +105,6 @@ registerHandler("session", function (clientId: string, message: ClientMessage) {
|
|
|
105
105
|
var activateMsg = message as SessionActivateMessage;
|
|
106
106
|
setActiveSession(clientId, activateMsg.projectSlug, activateMsg.sessionId);
|
|
107
107
|
setActiveProject(clientId, activateMsg.projectSlug);
|
|
108
|
-
watchSessionLock(activateMsg.sessionId);
|
|
109
108
|
var activateT0 = Date.now();
|
|
110
109
|
void loadSessionHistory(activateMsg.projectSlug, activateMsg.sessionId).then(function (historyResult) {
|
|
111
110
|
log.session("session:activate history: %dms", Date.now() - activateT0);
|
|
@@ -114,8 +113,6 @@ registerHandler("session", function (clientId: string, message: ClientMessage) {
|
|
|
114
113
|
if (interrupted) {
|
|
115
114
|
clearInterruptedFlag(activateMsg.sessionId);
|
|
116
115
|
}
|
|
117
|
-
var busy = isSessionBusy(activateMsg.sessionId);
|
|
118
|
-
var busyOwner = busy ? getBusyOwner(activateMsg.sessionId) : undefined;
|
|
119
116
|
sendTo(clientId, {
|
|
120
117
|
type: "session:history",
|
|
121
118
|
projectSlug: activateMsg.projectSlug,
|
|
@@ -123,8 +120,6 @@ registerHandler("session", function (clientId: string, message: ClientMessage) {
|
|
|
123
120
|
messages: historyResult.messages,
|
|
124
121
|
title: title,
|
|
125
122
|
interrupted: interrupted || undefined,
|
|
126
|
-
busy: busy || undefined,
|
|
127
|
-
busyOwner: busyOwner,
|
|
128
123
|
totalMessages: historyResult.totalMessages,
|
|
129
124
|
hasMore: historyResult.hasMore,
|
|
130
125
|
});
|
|
@@ -222,13 +217,4 @@ registerHandler("session", function (clientId: string, message: ClientMessage) {
|
|
|
222
217
|
});
|
|
223
218
|
}
|
|
224
219
|
|
|
225
|
-
if (message.type === "session:stop_external") {
|
|
226
|
-
var stopMsg = message as { type: string; sessionId: string };
|
|
227
|
-
var stopped = stopExternalSession(stopMsg.sessionId);
|
|
228
|
-
if (stopped) {
|
|
229
|
-
log.session("Sent SIGINT to external CLI process for session %s", stopMsg.sessionId);
|
|
230
|
-
} else {
|
|
231
|
-
sendTo(clientId, { type: "chat:error", message: "No external process found for this session." });
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
220
|
});
|
|
@@ -122,6 +122,9 @@ registerHandler("settings", function (clientId: string, message: ClientMessage)
|
|
|
122
122
|
},
|
|
123
123
|
projects: refreshed.projects,
|
|
124
124
|
};
|
|
125
|
+
if ("costBudget" in incoming && incoming.costBudget == null) {
|
|
126
|
+
delete (updated as Record<string, unknown>).costBudget;
|
|
127
|
+
}
|
|
125
128
|
saveConfig(updated);
|
|
126
129
|
var updatedWithClaudeMd = { ...updated, claudeMd: loadGlobalClaudeMd() };
|
|
127
130
|
sendTo(clientId, {
|
package/server/src/index.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
delete process.env.CLAUDECODE;
|
|
3
|
+
delete process.env.CLAUDE_CODE_ENTRYPOINT;
|
|
4
|
+
|
|
2
5
|
import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
3
6
|
import { join } from "node:path";
|
|
4
7
|
import { DAEMON_PID_FILE } from "@lattice/shared";
|