@cryptiklemur/lattice 1.46.7 → 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 +32 -14
- 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;
|
|
@@ -268,8 +277,6 @@ export function setActiveSession(projectSlug: string | null, sessionId: string |
|
|
|
268
277
|
promptSuggestion: null,
|
|
269
278
|
failedInput: null,
|
|
270
279
|
messageQueue: [],
|
|
271
|
-
isBusy: false,
|
|
272
|
-
busyOwner: null,
|
|
273
280
|
isPlanMode: false,
|
|
274
281
|
pendingPrefill: state.pendingPrefill,
|
|
275
282
|
};
|
|
@@ -333,8 +340,6 @@ export function clearSession(): void {
|
|
|
333
340
|
promptSuggestion: null,
|
|
334
341
|
failedInput: null,
|
|
335
342
|
messageQueue: [],
|
|
336
|
-
isBusy: false,
|
|
337
|
-
busyOwner: null,
|
|
338
343
|
isPlanMode: false,
|
|
339
344
|
pendingPrefill: null,
|
|
340
345
|
budgetStatus: null,
|
|
@@ -367,12 +372,6 @@ export function setFailedInput(text: string | null): void {
|
|
|
367
372
|
});
|
|
368
373
|
}
|
|
369
374
|
|
|
370
|
-
export function setSessionBusy(busy: boolean, owner?: "cli" | "lattice" | null): void {
|
|
371
|
-
sessionStore.setState(function (state) {
|
|
372
|
-
return { ...state, isBusy: busy, busyOwner: busy ? (owner ?? null) : null };
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
|
|
376
375
|
export function setIsPlanMode(active: boolean): void {
|
|
377
376
|
sessionStore.setState(function (state) {
|
|
378
377
|
return { ...state, isPlanMode: active };
|
|
@@ -385,6 +384,25 @@ export function setBudgetStatus(status: BudgetStatus | null): void {
|
|
|
385
384
|
});
|
|
386
385
|
}
|
|
387
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
|
+
|
|
388
406
|
export function setBudgetExceeded(exceeded: boolean): void {
|
|
389
407
|
sessionStore.setState(function (state) {
|
|
390
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";
|