@cryptiklemur/lattice 1.43.7 → 1.44.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.
@@ -3,6 +3,7 @@ import { X, Columns2, Rows2, MessageSquare, FolderOpen, TerminalSquare, StickyNo
3
3
  import { useWorkspace } from "../../hooks/useWorkspace";
4
4
  import { useSession } from "../../hooks/useSession";
5
5
  import type { Tab, TabType } from "../../stores/workspace";
6
+ import { pinTab } from "../../stores/workspace";
6
7
  import { formatSessionTitle } from "../../utils/formatSessionTitle";
7
8
 
8
9
  interface TabBarProps {
@@ -155,6 +156,7 @@ export function TabBar({ paneId, isActivePane }: TabBarProps) {
155
156
  tabIndex={0}
156
157
  aria-selected={isActive}
157
158
  onClick={function () { handleTabClick(tab.id); }}
159
+ onDoubleClick={function () { if (!tab.pinned) pinTab(tab.id); }}
158
160
  onMouseDown={function (e) { handleMiddleClick(e, tab); }}
159
161
  onKeyDown={function (e) {
160
162
  if (e.key === "Enter" || e.key === " ") {
@@ -171,7 +173,7 @@ export function TabBar({ paneId, isActivePane }: TabBarProps) {
171
173
  }
172
174
  >
173
175
  <Icon size={14} className={isActive ? "text-primary" : ""} />
174
- <span className="truncate text-[12px]">{label}</span>
176
+ <span className={"truncate text-[12px]" + (tab.pinned ? "" : " italic")}>{label}</span>
175
177
  {tab.closeable && (
176
178
  <button
177
179
  aria-label={"Close " + label + " tab"}
@@ -18,7 +18,7 @@ import type {
18
18
  } from "@lattice/shared";
19
19
  import { useWebSocket } from "./useWebSocket";
20
20
  import { setActiveSessionId as setSidebarSessionId } from "../stores/sidebar";
21
- import { updateSessionTabTitle } from "../stores/workspace";
21
+ import { updateSessionTabTitle, pinTab } from "../stores/workspace";
22
22
  import {
23
23
  getSessionStore,
24
24
  setSessionMessages,
@@ -120,6 +120,7 @@ export function useSession(): UseSessionReturn {
120
120
  setPromptSuggestion(null);
121
121
  setIsProcessing(true);
122
122
  setSessionBusy(false);
123
+ pinTab("chat-" + currentSessionId);
123
124
  sendRef.current(msg as ChatSendMessage);
124
125
  }
125
126
 
@@ -156,17 +156,19 @@ export function decodeWorkspaceUrl(
156
156
  type: "chat",
157
157
  label: "Session",
158
158
  closeable: true,
159
+ pinned: true,
159
160
  sessionId: resolvedId,
160
161
  projectSlug: projectSlug,
161
162
  };
162
163
  } else if (tabType === "chat") {
163
- tab = { id: "chat", type: "chat", label: "Chat", closeable: false };
164
+ tab = { id: "chat", type: "chat", label: "Chat", closeable: false, pinned: true };
164
165
  } else {
165
166
  tab = {
166
167
  id: tabType,
167
168
  type: tabType,
168
169
  label: labels[tabType],
169
170
  closeable: true,
171
+ pinned: true,
170
172
  };
171
173
  }
172
174
 
@@ -190,7 +192,7 @@ export function decodeWorkspaceUrl(
190
192
  }
191
193
 
192
194
  if (panes.length === 0) {
193
- const defaultTab: Tab = { id: "chat", type: "chat", label: "Chat", closeable: false };
195
+ const defaultTab: Tab = { id: "chat", type: "chat", label: "Chat", closeable: false, pinned: true };
194
196
  tabs.push(defaultTab);
195
197
  panes.push({ id: "pane-1", tabIds: ["chat"], activeTabId: "chat" });
196
198
  }
@@ -8,6 +8,7 @@ export interface Tab {
8
8
  type: TabType;
9
9
  label: string;
10
10
  closeable: boolean;
11
+ pinned: boolean;
11
12
  sessionId?: string;
12
13
  projectSlug?: string;
13
14
  }
@@ -26,7 +27,7 @@ export interface WorkspaceState {
26
27
  splitRatio: number;
27
28
  }
28
29
 
29
- var CHAT_TAB: Tab = { id: "chat", type: "chat", label: "Chat", closeable: false };
30
+ var CHAT_TAB: Tab = { id: "chat", type: "chat", label: "Chat", closeable: false, pinned: true };
30
31
 
31
32
  var DEFAULT_PANE: Pane = { id: "pane-1", tabIds: ["chat"], activeTabId: "chat" };
32
33
 
@@ -77,6 +78,7 @@ export function openTab(type: TabType): void {
77
78
  type: type,
78
79
  label: labels[type],
79
80
  closeable: type !== "chat",
81
+ pinned: true,
80
82
  };
81
83
 
82
84
  var defaultChat = state.tabs.find(function (t) { return t.id === "chat" && !t.sessionId; });
@@ -135,36 +137,55 @@ export function openSessionTab(sessionId: string, projectSlug: string, title: st
135
137
  type: "chat",
136
138
  label: title || "Session",
137
139
  closeable: true,
140
+ pinned: false,
138
141
  sessionId: sessionId,
139
142
  projectSlug: projectSlug,
140
143
  };
141
144
 
145
+ var previewTab = state.tabs.find(function (t) { return t.type === "chat" && t.sessionId && !t.pinned; });
142
146
  var hadDefaultChat = state.tabs.some(function (t) { return t.id === "chat"; });
143
147
 
144
- var newTabs = hadDefaultChat
145
- ? state.tabs.filter(function (t) { return t.id !== "chat"; }).concat([tab])
146
- : [...state.tabs, tab];
147
-
148
- var newPanes = state.panes.map(function (p) {
149
- var updatedTabIds = hadDefaultChat
150
- ? p.tabIds.map(function (id) { return id === "chat" ? tabId : id; })
151
- : (p.id === state.activePaneId ? [...p.tabIds, tabId] : p.tabIds);
148
+ var newTabs: Tab[];
149
+ var newPanes: Pane[];
152
150
 
153
- var needsActiveUpdate = hadDefaultChat
154
- ? (p.activeTabId === "chat" || p.id === state.activePaneId)
155
- : p.id === state.activePaneId;
151
+ if (previewTab) {
152
+ var oldId = previewTab.id;
153
+ newTabs = state.tabs.map(function (t) { return t.id === oldId ? tab : t; });
154
+ newPanes = state.panes.map(function (p) {
155
+ var updatedTabIds = p.tabIds.map(function (id) { return id === oldId ? tabId : id; });
156
+ return {
157
+ ...p,
158
+ tabIds: updatedTabIds,
159
+ activeTabId: p.activeTabId === oldId ? tabId : p.activeTabId,
160
+ };
161
+ });
162
+ } else if (hadDefaultChat) {
163
+ newTabs = state.tabs.filter(function (t) { return t.id !== "chat"; }).concat([tab]);
164
+ newPanes = state.panes.map(function (p) {
165
+ var updatedTabIds = p.tabIds.map(function (id) { return id === "chat" ? tabId : id; });
166
+ var needsActiveUpdate = p.activeTabId === "chat" || p.id === state.activePaneId;
167
+ return { ...p, tabIds: updatedTabIds, activeTabId: needsActiveUpdate ? tabId : p.activeTabId };
168
+ });
169
+ } else {
170
+ newTabs = [...state.tabs, tab];
171
+ newPanes = state.panes.map(function (p) {
172
+ return p.id === state.activePaneId
173
+ ? { ...p, tabIds: [...p.tabIds, tabId], activeTabId: tabId }
174
+ : p;
175
+ });
176
+ }
156
177
 
157
- return {
158
- ...p,
159
- tabIds: updatedTabIds,
160
- activeTabId: needsActiveUpdate ? tabId : p.activeTabId,
161
- };
162
- });
178
+ return { ...state, tabs: newTabs, panes: newPanes };
179
+ });
180
+ }
163
181
 
182
+ export function pinTab(tabId: string): void {
183
+ workspaceStore.setState(function (state) {
164
184
  return {
165
185
  ...state,
166
- tabs: newTabs,
167
- panes: newPanes,
186
+ tabs: state.tabs.map(function (t) {
187
+ return t.id === tabId ? { ...t, pinned: true } : t;
188
+ }),
168
189
  };
169
190
  });
170
191
  }
@@ -198,7 +219,7 @@ export function closeTab(tabId: string): void {
198
219
  var isLastChatTab = tab.type === "chat" && chatTabCount <= 1;
199
220
 
200
221
  if (isLastChatTab) {
201
- var replacementTab: Tab = { id: "chat", type: "chat", label: "Chat", closeable: false };
222
+ var replacementTab: Tab = { id: "chat", type: "chat", label: "Chat", closeable: false, pinned: true };
202
223
  var replacedTabs = state.tabs.map(function (t) {
203
224
  if (t.id === tabId) return replacementTab;
204
225
  return t;
@@ -236,7 +257,7 @@ export function closeTab(tabId: string): void {
236
257
 
237
258
  var hasEmptySinglePane = newPanes.length === 1 && newPanes[0].tabIds.length === 0;
238
259
  if (hasEmptySinglePane) {
239
- var defaultTab: Tab = { id: "chat", type: "chat", label: "Chat", closeable: false };
260
+ var defaultTab: Tab = { id: "chat", type: "chat", label: "Chat", closeable: false, pinned: true };
240
261
  return {
241
262
  tabs: [defaultTab],
242
263
  panes: [{ ...newPanes[0], tabIds: ["chat"], activeTabId: "chat" }],
@@ -468,7 +489,7 @@ export function loadWorkspaceForProject(projectSlug: string | null): void {
468
489
  urlSyncSuppressed = true;
469
490
  workspaceStore.setState(function () {
470
491
  return {
471
- tabs: [{ id: "chat", type: "chat" as TabType, label: "Chat", closeable: false }],
492
+ tabs: [{ id: "chat", type: "chat" as TabType, label: "Chat", closeable: false, pinned: true }],
472
493
  panes: [{ id: "pane-1", tabIds: ["chat"], activeTabId: "chat" }],
473
494
  activePaneId: "pane-1",
474
495
  splitDirection: null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cryptiklemur/lattice",
3
- "version": "1.43.7",
3
+ "version": "1.44.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>",
@@ -107,8 +107,13 @@ registerHandler("session", function (clientId: string, message: ClientMessage) {
107
107
  setActiveProject(clientId, activateMsg.projectSlug);
108
108
  watchSessionLock(activateMsg.sessionId);
109
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);
110
+ void Promise.all([
111
+ loadSessionHistory(activateMsg.projectSlug, activateMsg.sessionId),
112
+ getSessionTitle(activateMsg.projectSlug, activateMsg.sessionId).catch(function () { return null; }),
113
+ ]).then(function (results) {
114
+ log.session("session:activate history+title: %dms", Date.now() - activateT0);
115
+ var historyResult = results[0];
116
+ var title = results[1];
112
117
  var interrupted = wasSessionInterrupted(activateMsg.sessionId);
113
118
  if (interrupted) {
114
119
  clearInterruptedFlag(activateMsg.sessionId);
@@ -120,7 +125,7 @@ registerHandler("session", function (clientId: string, message: ClientMessage) {
120
125
  projectSlug: activateMsg.projectSlug,
121
126
  sessionId: activateMsg.sessionId,
122
127
  messages: historyResult.messages,
123
- title: null,
128
+ title: title,
124
129
  interrupted: interrupted || undefined,
125
130
  busy: busy || undefined,
126
131
  busyOwner: busyOwner,
@@ -133,15 +138,11 @@ registerHandler("session", function (clientId: string, message: ClientMessage) {
133
138
  });
134
139
 
135
140
  void Promise.all([
136
- getSessionTitle(activateMsg.projectSlug, activateMsg.sessionId).catch(function () { return null; }),
137
141
  getSessionUsage(activateMsg.projectSlug, activateMsg.sessionId).catch(function () { return null; }),
138
142
  getContextBreakdown(activateMsg.projectSlug, activateMsg.sessionId).catch(function () { return null; }),
139
143
  ]).then(function (results) {
140
144
  try {
141
- if (results[0]) {
142
- sendTo(clientId, { type: "session:history", projectSlug: activateMsg.projectSlug, sessionId: activateMsg.sessionId, messages: [], title: results[0] as string });
143
- }
144
- var usage = results[1];
145
+ var usage = results[0];
145
146
  if (usage) {
146
147
  sendTo(clientId, {
147
148
  type: "chat:context_usage",
@@ -156,7 +157,7 @@ registerHandler("session", function (clientId: string, message: ClientMessage) {
156
157
  log.session("Error sending context usage: %O", err);
157
158
  }
158
159
  try {
159
- var breakdown = results[2];
160
+ var breakdown = results[1];
160
161
  if (breakdown) {
161
162
  sendTo(clientId, {
162
163
  type: "chat:context_breakdown",