@cryptiklemur/lattice 1.43.5 → 1.43.7

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, busyOwner, isPlanMode, pendingPrefill, activateSession, budgetStatus, budgetExceeded, sendBudgetOverride, dismissBudgetExceeded } = useSession();
26
+ var { messages, isProcessing, sendMessage, activeSessionId, activeSessionTitle, currentStatus, contextUsage, contextBreakdown, lastResponseCost, lastResponseDuration, historyLoading, historyHasMore, loadMoreHistory, 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
 
@@ -735,6 +735,16 @@ export function ChatView({ sessionId: tabSessionId, projectSlug: tabProjectSlug
735
735
  aria-relevant="additions"
736
736
  style={{ WebkitOverflowScrolling: "touch", touchAction: "pan-y" }}
737
737
  >
738
+ {historyHasMore && messages.length > 0 && (
739
+ <div className="flex justify-center py-3">
740
+ <button
741
+ onClick={loadMoreHistory}
742
+ className="text-[11px] text-base-content/30 hover:text-base-content/50 font-mono transition-colors"
743
+ >
744
+ Load older messages
745
+ </button>
746
+ </div>
747
+ )}
738
748
  {messages.length === 0 && historyLoading ? (
739
749
  <div className="flex items-center justify-center h-full">
740
750
  <div className="flex flex-col items-center gap-3">
@@ -78,6 +78,8 @@ export interface UseSessionReturn extends SessionState {
78
78
  clearMessageQueue: () => void;
79
79
  sendBudgetOverride: () => void;
80
80
  dismissBudgetExceeded: () => void;
81
+ loadMoreHistory: () => void;
82
+ historyHasMore: boolean;
81
83
  }
82
84
 
83
85
  export function useSession(): UseSessionReturn {
@@ -276,6 +278,19 @@ export function useSession(): UseSessionReturn {
276
278
  updatePermissionStatus(m.requestId, m.status);
277
279
  }
278
280
 
281
+ function handleHistoryPage(msg: ServerMessage) {
282
+ var m = msg as { type: string; sessionId: string; messages: HistoryMessage[]; hasMore: boolean };
283
+ var state = getSessionStore().state;
284
+ if (m.sessionId !== state.activeSessionId) return;
285
+ getSessionStore().setState(function (s) {
286
+ return {
287
+ ...s,
288
+ messages: mergeToolResults(m.messages).concat(s.messages),
289
+ historyHasMore: m.hasMore,
290
+ };
291
+ });
292
+ }
293
+
279
294
  function handleHistory(msg: ServerMessage) {
280
295
  var m = msg as SessionHistoryMessage;
281
296
  setCurrentAssistantUuid(null);
@@ -303,6 +318,8 @@ export function useSession(): UseSessionReturn {
303
318
  lastResponseDuration: null,
304
319
  lastReadIndex: null,
305
320
  historyLoading: false,
321
+ historyHasMore: m.hasMore || false,
322
+ historyTotalMessages: m.totalMessages || m.messages.length,
306
323
  wasInterrupted: m.interrupted || false,
307
324
  isBusy: m.busy || false,
308
325
  busyOwner: m.busyOwner ?? null,
@@ -390,6 +407,7 @@ export function useSession(): UseSessionReturn {
390
407
  subscribe("chat:context_usage", handleContextUsage);
391
408
  subscribe("chat:context_breakdown", handleContextBreakdown);
392
409
  subscribe("session:history", handleHistory);
410
+ subscribe("session:history_page_result", handleHistoryPage);
393
411
  subscribe("chat:prompt_suggestion", handlePromptSuggestion);
394
412
  subscribe("session:busy", handleSessionBusy);
395
413
  subscribe("chat:prompt_request", handlePromptRequest);
@@ -413,6 +431,7 @@ export function useSession(): UseSessionReturn {
413
431
  unsubscribe("chat:context_usage", handleContextUsage);
414
432
  unsubscribe("chat:context_breakdown", handleContextBreakdown);
415
433
  unsubscribe("session:history", handleHistory);
434
+ unsubscribe("session:history_page_result", handleHistoryPage);
416
435
  unsubscribe("chat:prompt_suggestion", handlePromptSuggestion);
417
436
  unsubscribe("session:busy", handleSessionBusy);
418
437
  unsubscribe("chat:prompt_request", handlePromptRequest);
@@ -441,6 +460,15 @@ export function useSession(): UseSessionReturn {
441
460
  lastResponseDuration: state.lastResponseDuration,
442
461
  lastReadIndex: state.lastReadIndex,
443
462
  historyLoading: state.historyLoading,
463
+ historyHasMore: state.historyHasMore,
464
+ historyTotalMessages: state.historyTotalMessages,
465
+ loadMoreHistory: function () {
466
+ if (!state.historyHasMore || !state.activeSessionId) return;
467
+ var totalMessages = state.historyTotalMessages;
468
+ var loadedCount = state.messages.length;
469
+ var beforeIndex = totalMessages - loadedCount;
470
+ sendRef.current({ type: "session:history_page", sessionId: state.activeSessionId, before: beforeIndex, limit: 100 } as any);
471
+ },
444
472
  wasInterrupted: state.wasInterrupted,
445
473
  promptSuggestion: state.promptSuggestion,
446
474
  failedInput: state.failedInput,
@@ -42,6 +42,8 @@ export interface SessionState {
42
42
  lastResponseDuration: number | null;
43
43
  lastReadIndex: number | null;
44
44
  historyLoading: boolean;
45
+ historyHasMore: boolean;
46
+ historyTotalMessages: number;
45
47
  wasInterrupted: boolean;
46
48
  promptSuggestion: string | null;
47
49
  failedInput: string | null;
@@ -68,6 +70,8 @@ var sessionStore = new Store<SessionState>({
68
70
  lastResponseDuration: null,
69
71
  lastReadIndex: null,
70
72
  historyLoading: false,
73
+ historyHasMore: false,
74
+ historyTotalMessages: 0,
71
75
  wasInterrupted: false,
72
76
  promptSuggestion: null,
73
77
  failedInput: null,
@@ -293,6 +297,8 @@ export function clearSession(): void {
293
297
  lastResponseDuration: null,
294
298
  lastReadIndex: null,
295
299
  historyLoading: false,
300
+ historyHasMore: false,
301
+ historyTotalMessages: 0,
296
302
  wasInterrupted: false,
297
303
  promptSuggestion: null,
298
304
  failedInput: null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cryptiklemur/lattice",
3
- "version": "1.43.5",
3
+ "version": "1.43.7",
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>",
@@ -20,11 +20,12 @@ import {
20
20
  getSessionUsage,
21
21
  listSessions,
22
22
  invalidateSessionCache,
23
+ getSessionHistoryPage,
23
24
  loadSessionHistory,
24
25
  renameSession,
25
26
  } from "../project/session";
26
27
  import { getContextBreakdown } from "../project/context-breakdown";
27
- import { setActiveSession } from "./chat";
28
+ import { setActiveSession, getActiveSession } from "./chat";
28
29
  import { setActiveProject } from "./fs";
29
30
  import { wasSessionInterrupted, clearInterruptedFlag, isSessionBusy, watchSessionLock, stopExternalSession, getBusyOwner } from "../project/sdk-bridge";
30
31
  import { log } from "../logger";
@@ -79,6 +80,20 @@ registerHandler("session", function (clientId: string, message: ClientMessage) {
79
80
  return;
80
81
  }
81
82
 
83
+ if (message.type === "session:history_page") {
84
+ var pageMsg = message as { type: "session:history_page"; sessionId: string; before: number; limit: number };
85
+ var activeSession = getActiveSession(clientId);
86
+ void getSessionHistoryPage(pageMsg.sessionId, pageMsg.before, pageMsg.limit, activeSession?.projectSlug).then(function (page) {
87
+ sendTo(clientId, {
88
+ type: "session:history_page_result",
89
+ sessionId: pageMsg.sessionId,
90
+ messages: page.messages,
91
+ hasMore: page.hasMore,
92
+ });
93
+ });
94
+ return;
95
+ }
96
+
82
97
  if (message.type === "session:create") {
83
98
  var createMsg = message as SessionCreateMessage;
84
99
  var session = createSession(createMsg.projectSlug);
@@ -91,47 +106,42 @@ registerHandler("session", function (clientId: string, message: ClientMessage) {
91
106
  setActiveSession(clientId, activateMsg.projectSlug, activateMsg.sessionId);
92
107
  setActiveProject(clientId, activateMsg.projectSlug);
93
108
  watchSessionLock(activateMsg.sessionId);
109
+ var activateT0 = Date.now();
110
+ void loadSessionHistory(activateMsg.projectSlug, activateMsg.sessionId).then(function (historyResult) {
111
+ log.session("session:activate total history load: %dms", Date.now() - activateT0);
112
+ var interrupted = wasSessionInterrupted(activateMsg.sessionId);
113
+ if (interrupted) {
114
+ clearInterruptedFlag(activateMsg.sessionId);
115
+ }
116
+ var busy = isSessionBusy(activateMsg.sessionId);
117
+ var busyOwner = busy ? getBusyOwner(activateMsg.sessionId) : undefined;
118
+ sendTo(clientId, {
119
+ type: "session:history",
120
+ projectSlug: activateMsg.projectSlug,
121
+ sessionId: activateMsg.sessionId,
122
+ messages: historyResult.messages,
123
+ title: null,
124
+ interrupted: interrupted || undefined,
125
+ busy: busy || undefined,
126
+ busyOwner: busyOwner,
127
+ totalMessages: historyResult.totalMessages,
128
+ hasMore: historyResult.hasMore,
129
+ });
130
+ }).catch(function (err) {
131
+ log.session("Error sending session history: %O", err);
132
+ sendTo(clientId, { type: "chat:error", message: "Failed to load session history" });
133
+ });
134
+
94
135
  void Promise.all([
95
- loadSessionHistory(activateMsg.projectSlug, activateMsg.sessionId).catch(function (err) {
96
- log.session("Failed to load session history: %O", err);
97
- return null;
98
- }),
99
- getSessionTitle(activateMsg.projectSlug, activateMsg.sessionId).catch(function (err) {
100
- log.session("Failed to load session title: %O", err);
101
- return null;
102
- }),
103
- getSessionUsage(activateMsg.projectSlug, activateMsg.sessionId).catch(function (err) {
104
- log.session("Failed to load session usage: %O", err);
105
- return null;
106
- }),
107
- getContextBreakdown(activateMsg.projectSlug, activateMsg.sessionId).catch(function (err) {
108
- log.session("Failed to load context breakdown: %O", err);
109
- return null;
110
- }),
136
+ getSessionTitle(activateMsg.projectSlug, activateMsg.sessionId).catch(function () { return null; }),
137
+ getSessionUsage(activateMsg.projectSlug, activateMsg.sessionId).catch(function () { return null; }),
138
+ getContextBreakdown(activateMsg.projectSlug, activateMsg.sessionId).catch(function () { return null; }),
111
139
  ]).then(function (results) {
112
140
  try {
113
- var interrupted = wasSessionInterrupted(activateMsg.sessionId);
114
- if (interrupted) {
115
- clearInterruptedFlag(activateMsg.sessionId);
141
+ if (results[0]) {
142
+ sendTo(clientId, { type: "session:history", projectSlug: activateMsg.projectSlug, sessionId: activateMsg.sessionId, messages: [], title: results[0] as string });
116
143
  }
117
- var busy = isSessionBusy(activateMsg.sessionId);
118
- var busyOwner = busy ? getBusyOwner(activateMsg.sessionId) : undefined;
119
- sendTo(clientId, {
120
- type: "session:history",
121
- projectSlug: activateMsg.projectSlug,
122
- sessionId: activateMsg.sessionId,
123
- messages: results[0] || [],
124
- title: results[1],
125
- interrupted: interrupted || undefined,
126
- busy: busy || undefined,
127
- busyOwner: busyOwner,
128
- });
129
- } catch (err) {
130
- log.session("Error sending session history: %O", err);
131
- sendTo(clientId, { type: "chat:error", message: "Failed to load session history" });
132
- }
133
- try {
134
- var usage = results[2];
144
+ var usage = results[1];
135
145
  if (usage) {
136
146
  sendTo(clientId, {
137
147
  type: "chat:context_usage",
@@ -146,7 +156,7 @@ registerHandler("session", function (clientId: string, message: ClientMessage) {
146
156
  log.session("Error sending context usage: %O", err);
147
157
  }
148
158
  try {
149
- var breakdown = results[3];
159
+ var breakdown = results[2];
150
160
  if (breakdown) {
151
161
  sendTo(clientId, {
152
162
  type: "chat:context_breakdown",
@@ -457,19 +457,121 @@ export async function getSessionTitle(projectSlug: string, sessionId: string): P
457
457
  return "Untitled";
458
458
  }
459
459
 
460
- export async function loadSessionHistory(projectSlug: string, sessionId: string): Promise<HistoryMessage[]> {
460
+ var historyCache = new Map<string, { messages: HistoryMessage[]; time: number }>();
461
+ var HISTORY_CACHE_TTL = 60000;
462
+ var INITIAL_MESSAGE_COUNT = 100;
463
+ var TAIL_READ_BYTES = 2 * 1024 * 1024;
464
+
465
+ function getSessionFilePath(projectSlug: string, sessionId: string): string | null {
461
466
  var projectPath = getProjectPath(projectSlug);
462
- var options = projectPath ? { dir: projectPath } : undefined;
467
+ if (!projectPath) return null;
468
+ var hash = projectPathToHash(projectPath);
469
+ var filePath = join(homedir(), ".claude", "projects", hash, sessionId + ".jsonl");
470
+ return existsSync(filePath) ? filePath : null;
471
+ }
472
+
473
+ function readTailLines(filePath: string, maxBytes: number): { lines: string[]; isPartial: boolean; fileSize: number } {
474
+ var { openSync, readSync, fstatSync, closeSync } = require("node:fs") as typeof import("node:fs");
475
+ var fd = openSync(filePath, "r");
476
+ var stat = fstatSync(fd);
477
+ var readStart = Math.max(0, stat.size - maxBytes);
478
+ var buf = Buffer.alloc(stat.size - readStart);
479
+ readSync(fd, buf, 0, buf.length, readStart);
480
+ closeSync(fd);
481
+ var text = buf.toString("utf-8");
482
+ if (readStart > 0) {
483
+ var firstNewline = text.indexOf("\n");
484
+ if (firstNewline >= 0) text = text.slice(firstNewline + 1);
485
+ }
486
+ var lines = text.split("\n").filter(function (l) { return l.length > 0; });
487
+ return { lines, isPartial: readStart > 0, fileSize: stat.size };
488
+ }
463
489
 
490
+ function parseJsonlLines(lines: string[]): SessionMessage[] {
491
+ var results: SessionMessage[] = [];
492
+ for (var i = 0; i < lines.length; i++) {
493
+ try {
494
+ var parsed = JSON.parse(lines[i]);
495
+ if (parsed.type === "user" || parsed.type === "assistant" || parsed.type === "system") {
496
+ results.push(parsed as SessionMessage);
497
+ }
498
+ } catch {}
499
+ }
500
+ return results;
501
+ }
502
+
503
+ export async function loadSessionHistory(projectSlug: string, sessionId: string): Promise<{ messages: HistoryMessage[]; totalMessages: number; hasMore: boolean }> {
464
504
  try {
465
- var messages = await getSessionMessages(sessionId, options);
466
- return convertSessionMessages(messages);
505
+ var t0 = Date.now();
506
+ var cached = historyCache.get(sessionId);
507
+ if (cached && Date.now() - cached.time < HISTORY_CACHE_TTL) {
508
+ var tail = cached.messages.length > INITIAL_MESSAGE_COUNT
509
+ ? cached.messages.slice(cached.messages.length - INITIAL_MESSAGE_COUNT)
510
+ : cached.messages;
511
+ log.session("loadSessionHistory %s: %dms (cached, %d total)", sessionId.slice(0, 8), Date.now() - t0, cached.messages.length);
512
+ return { messages: tail, totalMessages: cached.messages.length, hasMore: cached.messages.length > INITIAL_MESSAGE_COUNT };
513
+ }
514
+
515
+ var filePath = getSessionFilePath(projectSlug, sessionId);
516
+ if (filePath) {
517
+ var tailData = readTailLines(filePath, TAIL_READ_BYTES);
518
+ var tailRaw = parseJsonlLines(tailData.lines);
519
+ var tailMessages = convertSessionMessages(tailRaw);
520
+ var hasMore = tailData.isPartial;
521
+
522
+ log.session("loadSessionHistory %s: %dms (tail read, %d msgs, partial=%s)", sessionId.slice(0, 8), Date.now() - t0, tailMessages.length, hasMore);
523
+
524
+ if (!hasMore) {
525
+ historyCache.set(sessionId, { messages: tailMessages, time: Date.now() });
526
+ }
527
+
528
+ var initialSlice = tailMessages.length > INITIAL_MESSAGE_COUNT
529
+ ? tailMessages.slice(tailMessages.length - INITIAL_MESSAGE_COUNT)
530
+ : tailMessages;
531
+
532
+ return { messages: initialSlice, totalMessages: tailMessages.length, hasMore: hasMore };
533
+ }
534
+
535
+ var projectPath = getProjectPath(projectSlug);
536
+ var options = projectPath ? { dir: projectPath } : undefined;
537
+ var rawMessages = await getSessionMessages(sessionId, options);
538
+ var allMessages = convertSessionMessages(rawMessages);
539
+ historyCache.set(sessionId, { messages: allMessages, time: Date.now() });
540
+ log.session("loadSessionHistory %s: %dms (full SDK, %d msgs)", sessionId.slice(0, 8), Date.now() - t0, allMessages.length);
541
+ var tailSlice = allMessages.length > INITIAL_MESSAGE_COUNT
542
+ ? allMessages.slice(allMessages.length - INITIAL_MESSAGE_COUNT)
543
+ : allMessages;
544
+ return { messages: tailSlice, totalMessages: allMessages.length, hasMore: allMessages.length > INITIAL_MESSAGE_COUNT };
467
545
  } catch (err) {
468
546
  log.session("Failed to load session history: %O", err);
469
- return [];
547
+ return { messages: [], totalMessages: 0, hasMore: false };
470
548
  }
471
549
  }
472
550
 
551
+ export async function getSessionHistoryPage(sessionId: string, beforeIndex: number, limit: number, projectSlug?: string): Promise<{ messages: HistoryMessage[]; hasMore: boolean }> {
552
+ var cached = historyCache.get(sessionId);
553
+ if (!cached && projectSlug) {
554
+ var projectPath = getProjectPath(projectSlug);
555
+ var options = projectPath ? { dir: projectPath } : undefined;
556
+ try {
557
+ var rawMessages = await getSessionMessages(sessionId, options);
558
+ var allMessages = convertSessionMessages(rawMessages);
559
+ historyCache.set(sessionId, { messages: allMessages, time: Date.now() });
560
+ cached = { messages: allMessages, time: Date.now() };
561
+ log.session("getSessionHistoryPage: full load for %s, %d messages", sessionId.slice(0, 8), allMessages.length);
562
+ } catch {
563
+ return { messages: [], hasMore: false };
564
+ }
565
+ }
566
+ if (!cached) return { messages: [], hasMore: false };
567
+
568
+ var endIdx = Math.max(0, beforeIndex);
569
+ var startIdx = Math.max(0, endIdx - limit);
570
+ var page = cached.messages.slice(startIdx, endIdx);
571
+
572
+ return { messages: page, hasMore: startIdx > 0 };
573
+ }
574
+
473
575
  export function createSession(projectSlug: string): SessionSummary {
474
576
  var sessionId = randomUUID();
475
577
  var now = Date.now();
@@ -50,6 +50,20 @@ export interface SessionListRequestMessage {
50
50
  limit?: number;
51
51
  }
52
52
 
53
+ export interface SessionHistoryPageMessage {
54
+ type: "session:history_page";
55
+ sessionId: string;
56
+ before: number;
57
+ limit: number;
58
+ }
59
+
60
+ export interface SessionHistoryPageResultMessage {
61
+ type: "session:history_page_result";
62
+ sessionId: string;
63
+ messages: HistoryMessage[];
64
+ hasMore: boolean;
65
+ }
66
+
53
67
  export interface SessionPreviewRequestMessage {
54
68
  type: "session:preview_request";
55
69
  projectSlug: string;
@@ -578,6 +592,7 @@ export type ClientMessage =
578
592
  | EditorEnsureProjectMessage
579
593
  | ChatPromptResponseMessage
580
594
  | AnalyticsRequestMessage
595
+ | SessionHistoryPageMessage
581
596
  | SessionPreviewRequestMessage
582
597
  | BookmarkListMessage
583
598
  | BookmarkAddMessage
@@ -623,6 +638,8 @@ export interface SessionHistoryMessage {
623
638
  interrupted?: boolean;
624
639
  busy?: boolean;
625
640
  busyOwner?: "cli" | "lattice";
641
+ totalMessages?: number;
642
+ hasMore?: boolean;
626
643
  }
627
644
 
628
645
  export interface SessionBusyMessage {
@@ -1076,6 +1093,7 @@ export type ServerMessage =
1076
1093
  | AnalyticsDataMessage
1077
1094
  | AnalyticsErrorMessage
1078
1095
  | SessionPreviewMessage
1096
+ | SessionHistoryPageResultMessage
1079
1097
  | BookmarkListResultMessage
1080
1098
  | BudgetStatusMessage
1081
1099
  | BudgetExceededMessage