@cryptiklemur/lattice 1.40.8 → 1.41.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.
|
@@ -23,7 +23,7 @@ import { useBookmarks } from "../../hooks/useBookmarks";
|
|
|
23
23
|
import { formatSessionTitle } from "../../utils/formatSessionTitle";
|
|
24
24
|
|
|
25
25
|
export function ChatView({ sessionId: tabSessionId, projectSlug: tabProjectSlug }: { sessionId?: string; projectSlug?: string } = {}) {
|
|
26
|
-
var { messages, isProcessing, sendMessage, activeSessionId, activeSessionTitle, currentStatus, contextUsage, contextBreakdown, lastResponseCost, lastResponseDuration, historyLoading, wasInterrupted, promptSuggestion, failedInput, clearFailedInput, messageQueue, enqueueMessage, removeQueuedMessage, updateQueuedMessage, isBusy, isPlanMode, pendingPrefill, activateSession, budgetStatus, budgetExceeded, sendBudgetOverride, dismissBudgetExceeded } = useSession();
|
|
26
|
+
var { messages, isProcessing, sendMessage, activeSessionId, activeSessionTitle, currentStatus, contextUsage, contextBreakdown, lastResponseCost, lastResponseDuration, historyLoading, wasInterrupted, promptSuggestion, failedInput, clearFailedInput, messageQueue, enqueueMessage, removeQueuedMessage, updateQueuedMessage, isBusy, busyOwner, isPlanMode, pendingPrefill, activateSession, budgetStatus, budgetExceeded, sendBudgetOverride, dismissBudgetExceeded } = useSession();
|
|
27
27
|
var { activeProject } = useProjects();
|
|
28
28
|
var { toggleDrawer } = useSidebar();
|
|
29
29
|
|
|
@@ -955,7 +955,11 @@ export function ChatView({ sessionId: tabSessionId, projectSlug: tabProjectSlug
|
|
|
955
955
|
{isBusy && !isProcessing && (
|
|
956
956
|
<div className="flex items-center gap-2 px-3 sm:px-5 py-2 bg-info/10 border-t border-info/20">
|
|
957
957
|
<Terminal size={13} className="text-info flex-shrink-0" />
|
|
958
|
-
<span className="text-[12px] text-info flex-1">
|
|
958
|
+
<span className="text-[12px] text-info flex-1">
|
|
959
|
+
{busyOwner === "cli"
|
|
960
|
+
? "This session is controlled by Claude Code CLI — input is disabled"
|
|
961
|
+
: "This session is in use by another Lattice client — input is disabled"}
|
|
962
|
+
</span>
|
|
959
963
|
<button
|
|
960
964
|
onClick={function () { setConfirmStopExternal(true); }}
|
|
961
965
|
className="btn btn-ghost btn-xs text-error/70 hover:text-error gap-1 flex-shrink-0"
|
|
@@ -1063,7 +1067,9 @@ export function ChatView({ sessionId: tabSessionId, projectSlug: tabProjectSlug
|
|
|
1063
1067
|
disabled={!activeSessionId || !online || isBusy || (budgetStatus !== null && budgetStatus.enforcement === "hard-block" && budgetStatus.dailySpend >= budgetStatus.dailyLimit)}
|
|
1064
1068
|
disabledPlaceholder={
|
|
1065
1069
|
isBusy
|
|
1066
|
-
?
|
|
1070
|
+
? (busyOwner === "cli"
|
|
1071
|
+
? "Controlled by Claude Code CLI"
|
|
1072
|
+
: "Session in use by another Lattice client")
|
|
1067
1073
|
: (budgetStatus !== null && budgetStatus.enforcement === "hard-block" && budgetStatus.dailySpend >= budgetStatus.dailyLimit)
|
|
1068
1074
|
? "Daily budget exceeded ($" + budgetStatus.dailySpend.toFixed(2) + " / $" + budgetStatus.dailyLimit.toFixed(2) + ")"
|
|
1069
1075
|
: undefined
|
|
@@ -305,6 +305,7 @@ export function useSession(): UseSessionReturn {
|
|
|
305
305
|
historyLoading: false,
|
|
306
306
|
wasInterrupted: m.interrupted || false,
|
|
307
307
|
isBusy: m.busy || false,
|
|
308
|
+
busyOwner: m.busyOwner ?? null,
|
|
308
309
|
isPlanMode: false,
|
|
309
310
|
};
|
|
310
311
|
});
|
|
@@ -335,13 +336,13 @@ export function useSession(): UseSessionReturn {
|
|
|
335
336
|
}
|
|
336
337
|
|
|
337
338
|
function handleSessionBusy(msg: ServerMessage) {
|
|
338
|
-
var m = msg as { type: string; sessionId: string; busy: boolean };
|
|
339
|
+
var m = msg as { type: string; sessionId: string; busy: boolean; busyOwner?: "cli" | "lattice" };
|
|
339
340
|
var sessionState = getSessionStore().state;
|
|
340
341
|
if (m.sessionId === sessionState.activeSessionId) {
|
|
341
342
|
if (m.busy && sessionState.isProcessing) {
|
|
342
343
|
return;
|
|
343
344
|
}
|
|
344
|
-
setSessionBusy(m.busy);
|
|
345
|
+
setSessionBusy(m.busy, m.busyOwner);
|
|
345
346
|
}
|
|
346
347
|
}
|
|
347
348
|
|
|
@@ -445,6 +446,7 @@ export function useSession(): UseSessionReturn {
|
|
|
445
446
|
failedInput: state.failedInput,
|
|
446
447
|
messageQueue: state.messageQueue,
|
|
447
448
|
isBusy: state.isBusy,
|
|
449
|
+
busyOwner: state.busyOwner,
|
|
448
450
|
isPlanMode: state.isPlanMode,
|
|
449
451
|
pendingPrefill: state.pendingPrefill,
|
|
450
452
|
budgetStatus: state.budgetStatus,
|
|
@@ -47,6 +47,7 @@ export interface SessionState {
|
|
|
47
47
|
failedInput: string | null;
|
|
48
48
|
messageQueue: string[];
|
|
49
49
|
isBusy: boolean;
|
|
50
|
+
busyOwner: "cli" | "lattice" | null;
|
|
50
51
|
isPlanMode: boolean;
|
|
51
52
|
pendingPrefill: string | null;
|
|
52
53
|
budgetStatus: BudgetStatus | null;
|
|
@@ -72,6 +73,7 @@ var sessionStore = new Store<SessionState>({
|
|
|
72
73
|
failedInput: null,
|
|
73
74
|
messageQueue: [],
|
|
74
75
|
isBusy: false,
|
|
76
|
+
busyOwner: null,
|
|
75
77
|
isPlanMode: false,
|
|
76
78
|
pendingPrefill: null,
|
|
77
79
|
budgetStatus: null,
|
|
@@ -296,6 +298,7 @@ export function clearSession(): void {
|
|
|
296
298
|
failedInput: null,
|
|
297
299
|
messageQueue: [],
|
|
298
300
|
isBusy: false,
|
|
301
|
+
busyOwner: null,
|
|
299
302
|
isPlanMode: false,
|
|
300
303
|
pendingPrefill: null,
|
|
301
304
|
budgetStatus: null,
|
|
@@ -328,9 +331,9 @@ export function setFailedInput(text: string | null): void {
|
|
|
328
331
|
});
|
|
329
332
|
}
|
|
330
333
|
|
|
331
|
-
export function setSessionBusy(busy: boolean): void {
|
|
334
|
+
export function setSessionBusy(busy: boolean, owner?: "cli" | "lattice" | null): void {
|
|
332
335
|
sessionStore.setState(function (state) {
|
|
333
|
-
return { ...state, isBusy: busy };
|
|
336
|
+
return { ...state, isBusy: busy, busyOwner: busy ? (owner ?? null) : null };
|
|
334
337
|
});
|
|
335
338
|
}
|
|
336
339
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cryptiklemur/lattice",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.41.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>",
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
import { getContextBreakdown } from "../project/context-breakdown";
|
|
25
25
|
import { setActiveSession } from "./chat";
|
|
26
26
|
import { setActiveProject } from "./fs";
|
|
27
|
-
import { wasSessionInterrupted, clearInterruptedFlag, isSessionBusy, watchSessionLock, stopExternalSession } from "../project/sdk-bridge";
|
|
27
|
+
import { wasSessionInterrupted, clearInterruptedFlag, isSessionBusy, watchSessionLock, stopExternalSession, getBusyOwner } from "../project/sdk-bridge";
|
|
28
28
|
import { log } from "../logger";
|
|
29
29
|
|
|
30
30
|
registerHandler("session", function (clientId: string, message: ClientMessage) {
|
|
@@ -113,6 +113,7 @@ registerHandler("session", function (clientId: string, message: ClientMessage) {
|
|
|
113
113
|
clearInterruptedFlag(activateMsg.sessionId);
|
|
114
114
|
}
|
|
115
115
|
var busy = isSessionBusy(activateMsg.sessionId);
|
|
116
|
+
var busyOwner = busy ? getBusyOwner(activateMsg.sessionId) : undefined;
|
|
116
117
|
sendTo(clientId, {
|
|
117
118
|
type: "session:history",
|
|
118
119
|
projectSlug: activateMsg.projectSlug,
|
|
@@ -121,6 +122,7 @@ registerHandler("session", function (clientId: string, message: ClientMessage) {
|
|
|
121
122
|
title: results[1],
|
|
122
123
|
interrupted: interrupted || undefined,
|
|
123
124
|
busy: busy || undefined,
|
|
125
|
+
busyOwner: busyOwner,
|
|
124
126
|
});
|
|
125
127
|
} catch (err) {
|
|
126
128
|
log.session("Error sending session history: %O", err);
|
|
@@ -68,6 +68,7 @@ var interruptedSessions = new Set<string>();
|
|
|
68
68
|
// Track external lock state so we only broadcast on changes
|
|
69
69
|
var externalLockState = new Map<string, boolean>();
|
|
70
70
|
var watchedSessions = new Set<string>();
|
|
71
|
+
var remoteSessionWatchers = new Map<string, Set<string>>();
|
|
71
72
|
|
|
72
73
|
/**
|
|
73
74
|
* Start polling a session's lock file for external CLI usage.
|
|
@@ -85,10 +86,23 @@ export function unwatchSessionLock(sessionId: string): void {
|
|
|
85
86
|
externalLockState.delete(sessionId);
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
export function addRemoteSessionWatcher(sessionId: string, nodeId: string): void {
|
|
90
|
+
var watchers = remoteSessionWatchers.get(sessionId);
|
|
91
|
+
if (!watchers) {
|
|
92
|
+
watchers = new Set();
|
|
93
|
+
remoteSessionWatchers.set(sessionId, watchers);
|
|
94
|
+
}
|
|
95
|
+
watchers.add(nodeId);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function getBusyOwner(sessionId: string): "cli" | "lattice" | undefined {
|
|
99
|
+
if (!isSessionLockedByExternal(sessionId)) return undefined;
|
|
100
|
+
return "cli";
|
|
101
|
+
}
|
|
102
|
+
|
|
88
103
|
// Poll every 3 seconds for external lock changes
|
|
89
104
|
setInterval(function () {
|
|
90
105
|
for (var sessionId of watchedSessions) {
|
|
91
|
-
// Skip sessions with active Lattice streams — those are already tracked
|
|
92
106
|
if (activeStreams.has(sessionId)) continue;
|
|
93
107
|
|
|
94
108
|
var locked = isSessionLockedByExternal(sessionId);
|
|
@@ -96,7 +110,24 @@ setInterval(function () {
|
|
|
96
110
|
|
|
97
111
|
if (locked !== prev) {
|
|
98
112
|
externalLockState.set(sessionId, locked);
|
|
99
|
-
|
|
113
|
+
var owner = locked ? getBusyOwner(sessionId) : undefined;
|
|
114
|
+
broadcast({ type: "session:busy", sessionId, busy: locked, busyOwner: owner });
|
|
115
|
+
|
|
116
|
+
var watchers = remoteSessionWatchers.get(sessionId);
|
|
117
|
+
if (watchers) {
|
|
118
|
+
var { getPeerConnection } = require("../mesh/connector") as typeof import("../mesh/connector");
|
|
119
|
+
for (var nodeId of watchers) {
|
|
120
|
+
var peerWs = getPeerConnection(nodeId);
|
|
121
|
+
if (peerWs) {
|
|
122
|
+
peerWs.send(JSON.stringify({
|
|
123
|
+
type: "mesh:proxy_response",
|
|
124
|
+
projectSlug: "",
|
|
125
|
+
requestId: "busy-" + sessionId,
|
|
126
|
+
payload: { type: "session:busy", sessionId, busy: locked, busyOwner: owner },
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
100
131
|
}
|
|
101
132
|
}
|
|
102
133
|
}, 3000);
|
package/shared/src/messages.ts
CHANGED
|
@@ -622,12 +622,14 @@ export interface SessionHistoryMessage {
|
|
|
622
622
|
title?: string;
|
|
623
623
|
interrupted?: boolean;
|
|
624
624
|
busy?: boolean;
|
|
625
|
+
busyOwner?: "cli" | "lattice";
|
|
625
626
|
}
|
|
626
627
|
|
|
627
628
|
export interface SessionBusyMessage {
|
|
628
629
|
type: "session:busy";
|
|
629
630
|
sessionId: string;
|
|
630
631
|
busy: boolean;
|
|
632
|
+
busyOwner?: "cli" | "lattice";
|
|
631
633
|
}
|
|
632
634
|
|
|
633
635
|
export interface ChatUserMessage {
|