clay-server 2.33.0-beta.2 → 2.33.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.
@@ -33,6 +33,19 @@ var countdownContainer = null;
33
33
  var sessionCtxMenu = null;
34
34
  var sessionCtxSessionId = null;
35
35
 
36
+ function sendSessionBookmark(sessionId, bookmarked) {
37
+ if (getWs() && store.get('connected')) {
38
+ getWs().send(JSON.stringify({ type: "set_session_bookmark", sessionId: sessionId, bookmarked: !!bookmarked }));
39
+ }
40
+ }
41
+
42
+ function compareSessionListItems(a, b) {
43
+ var aBookmarked = !!a.bookmarked;
44
+ var bBookmarked = !!b.bookmarked;
45
+ if (aBookmarked !== bBookmarked) return aBookmarked ? -1 : 1;
46
+ return (b.lastActivity || 0) - (a.lastActivity || 0);
47
+ }
48
+
36
49
  export function initSidebarSessions() {
37
50
 
38
51
  document.addEventListener("click", function () { closeSessionCtxMenu(); });
@@ -167,6 +180,16 @@ function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData) {
167
180
  var menu = document.createElement("div");
168
181
  menu.className = "session-ctx-menu";
169
182
 
183
+ var bookmarkItem = document.createElement("button");
184
+ bookmarkItem.className = "session-ctx-item";
185
+ bookmarkItem.innerHTML = iconHtml(sessionData && sessionData.bookmarked ? "star-off" : "star") + " <span>" + (sessionData && sessionData.bookmarked ? "Remove Bookmark" : "Bookmark") + "</span>";
186
+ bookmarkItem.addEventListener("click", function (e) {
187
+ e.stopPropagation();
188
+ closeSessionCtxMenu();
189
+ sendSessionBookmark(sessionId, !(sessionData && sessionData.bookmarked));
190
+ });
191
+ menu.appendChild(bookmarkItem);
192
+
170
193
  var renameItem = document.createElement("button");
171
194
  renameItem.className = "session-ctx-item";
172
195
  renameItem.innerHTML = iconHtml("pencil") + " <span>Rename</span>";
@@ -654,6 +677,23 @@ function renderSessionItem(s) {
654
677
  el.className = "session-item" + (s.active ? " active" : "") + (isMatch ? " search-match" : "") + (dimmed ? " search-dimmed" : "");
655
678
  el.dataset.sessionId = s.id;
656
679
 
680
+ var bookmarkBtn = document.createElement("button");
681
+ bookmarkBtn.className = "session-bookmark-btn" + (s.bookmarked ? " bookmarked inline" : " hover");
682
+ bookmarkBtn.type = "button";
683
+ bookmarkBtn.title = s.bookmarked ? "Remove bookmark" : "Bookmark";
684
+ bookmarkBtn.setAttribute("aria-label", s.bookmarked ? "Remove bookmark" : "Bookmark");
685
+ bookmarkBtn.innerHTML = iconHtml("star");
686
+ bookmarkBtn.addEventListener("click", (function (id, bookmarked) {
687
+ return function (e) {
688
+ e.preventDefault();
689
+ e.stopPropagation();
690
+ sendSessionBookmark(id, !bookmarked);
691
+ };
692
+ })(s.id, !!s.bookmarked));
693
+ if (s.bookmarked) {
694
+ el.appendChild(bookmarkBtn);
695
+ }
696
+
657
697
  var textSpan = document.createElement("span");
658
698
  textSpan.className = "session-item-text";
659
699
  var textHtml = "";
@@ -689,6 +729,10 @@ function renderSessionItem(s) {
689
729
  }
690
730
  el.appendChild(unreadBadge);
691
731
 
732
+ if (!s.bookmarked) {
733
+ el.appendChild(bookmarkBtn);
734
+ }
735
+
692
736
  el.addEventListener("click", (function (id) {
693
737
  return function () {
694
738
  if (getWs() && store.get('connected')) {
@@ -758,13 +802,33 @@ export function renderSessionList(sessions) {
758
802
  }
759
803
 
760
804
  // Sort by lastActivity descending
761
- items.sort(function (a, b) {
762
- return (b.lastActivity || 0) - (a.lastActivity || 0);
763
- });
805
+ items.sort(compareSessionListItems);
764
806
 
765
- var currentGroup = "";
807
+ var bookmarkedItems = [];
808
+ var regularItems = [];
766
809
  for (var n = 0; n < items.length; n++) {
767
810
  var item = items[n];
811
+ if (item.type === "session" && item.data && item.data.bookmarked) {
812
+ bookmarkedItems.push(item);
813
+ } else {
814
+ regularItems.push(item);
815
+ }
816
+ }
817
+
818
+ if (bookmarkedItems.length > 0) {
819
+ var bookmarkedHeader = document.createElement("div");
820
+ bookmarkedHeader.className = "session-group-header";
821
+ bookmarkedHeader.textContent = "Bookmarked";
822
+ getSessionListEl().appendChild(bookmarkedHeader);
823
+
824
+ for (var bi = 0; bi < bookmarkedItems.length; bi++) {
825
+ getSessionListEl().appendChild(renderSessionItem(bookmarkedItems[bi].data));
826
+ }
827
+ }
828
+
829
+ var currentGroup = "";
830
+ for (var ri = 0; ri < regularItems.length; ri++) {
831
+ var item = regularItems[ri];
768
832
  var group = getDateGroup(item.lastActivity || 0);
769
833
  if (group !== currentGroup) {
770
834
  currentGroup = group;
@@ -101,10 +101,13 @@ export function initSidebar(_ctx) {
101
101
  }
102
102
  });
103
103
 
104
- // --- New Ralph Loop button ---
105
- var newRalphBtn = ctx.$("new-ralph-btn");
106
- if (newRalphBtn) {
107
- newRalphBtn.addEventListener("click", function () {
104
+ // --- Loop (Ralph wizard) tool-palette tile ---
105
+ // The tile is rendered by tool-palette.js at the stable id
106
+ // "loop-tool-btn"; we just wire the click to the wizard opener
107
+ // the same way the old header pill did.
108
+ var loopBtn = ctx.$("loop-tool-btn");
109
+ if (loopBtn) {
110
+ loopBtn.addEventListener("click", function () {
108
111
  if (ctx.openRalphWizard) ctx.openRalphWizard();
109
112
  });
110
113
  }
@@ -24,6 +24,7 @@ var SESSION_TOOLS = [
24
24
  { id: "terminal-sidebar-btn", icon: "square-terminal", label: "Terminal", countId: "terminal-sidebar-count" },
25
25
  { id: "sticky-notes-sidebar-btn", icon: "sticky-note", label: "Sticky Notes", countId: "sticky-notes-sidebar-count" },
26
26
  { id: "scheduler-btn", icon: "calendar-clock", label: "Scheduled Tasks" },
27
+ { id: "loop-tool-btn", icon: "repeat", label: "Loop" },
27
28
  { id: "email-sidebar-btn", icon: "mail", label: "Email" },
28
29
  { id: "mcp-btn", icon: "cable", label: "MCP Servers", countId: "mcp-sidebar-count" },
29
30
  { id: "skills-btn", icon: "puzzle", label: "Skills" },
@@ -33,10 +34,11 @@ var MATE_TOOLS = [
33
34
  { id: "mate-memory-btn", icon: "brain", label: "Memory", countId: "mate-memory-count" },
34
35
  { id: "mate-knowledge-btn", icon: "book-open", label: "Knowledge", countId: "mate-knowledge-count" },
35
36
  { id: "mate-sticky-notes-btn", icon: "sticky-note", label: "Sticky Notes" },
37
+ { id: "mate-scheduler-btn", icon: "calendar-clock", label: "Scheduled Tasks" },
38
+ { id: "mate-debate-btn", icon: "mic", label: "Debate" },
36
39
  { id: "mate-email-btn", icon: "mail", label: "Email" },
37
40
  { id: "mate-mcp-btn", icon: "cable", label: "MCP Servers", countId: "mate-mcp-sidebar-count" },
38
41
  { id: "mate-skills-btn", icon: "puzzle", label: "Skills" },
39
- { id: "mate-scheduler-btn", icon: "calendar-clock", label: "Scheduled Tasks" },
40
42
  ];
41
43
 
42
44
  var PALETTES = {
@@ -13,6 +13,7 @@ var ctx;
13
13
  // --- Plan mode state ---
14
14
  var inPlanMode = false;
15
15
  var planContent = null;
16
+ var currentPlanCardEl = null;
16
17
 
17
18
  // --- Todo state ---
18
19
  var todoItems = [];
@@ -1169,6 +1170,7 @@ export function renderPlanBanner(type) {
1169
1170
  if (type === "enter") {
1170
1171
  inPlanMode = true;
1171
1172
  planContent = null;
1173
+ currentPlanCardEl = null;
1172
1174
  el.innerHTML =
1173
1175
  '<span class="plan-banner-icon">' + iconHtml("map") + '</span>' +
1174
1176
  '<span class="plan-banner-text">Entered plan mode</span>' +
@@ -1191,27 +1193,47 @@ export function renderPlanBanner(type) {
1191
1193
  export function renderPlanCard(content) {
1192
1194
  ctx.finalizeAssistantBlock();
1193
1195
  closeToolGroup();
1196
+ planContent = content;
1197
+
1198
+ var el = currentPlanCardEl && currentPlanCardEl.isConnected ? currentPlanCardEl : null;
1199
+ var header;
1200
+ var body;
1201
+ var isNew = !el;
1202
+ if (!el) {
1203
+ el = document.createElement("div");
1204
+ el.className = "plan-card";
1205
+
1206
+ header = document.createElement("div");
1207
+ header.className = "plan-card-header";
1208
+ header.innerHTML =
1209
+ '<span class="plan-card-icon">' + iconHtml("file-text") + '</span>' +
1210
+ '<span class="plan-card-title">Implementation Plan</span>' +
1211
+ '<button class="plan-card-copy" title="Copy plan">' + iconHtml("copy") + '</button>' +
1212
+ '<span class="plan-card-chevron">' + iconHtml("chevron-down") + '</span>';
1213
+
1214
+ body = document.createElement("div");
1215
+ body.className = "plan-card-body";
1216
+
1217
+ header.addEventListener("click", function () {
1218
+ el.classList.toggle("collapsed");
1219
+ });
1194
1220
 
1195
- var el = document.createElement("div");
1196
- el.className = "plan-card";
1197
-
1198
- var header = document.createElement("div");
1199
- header.className = "plan-card-header";
1200
- header.innerHTML =
1201
- '<span class="plan-card-icon">' + iconHtml("file-text") + '</span>' +
1202
- '<span class="plan-card-title">Implementation Plan</span>' +
1203
- '<button class="plan-card-copy" title="Copy plan">' + iconHtml("copy") + '</button>' +
1204
- '<span class="plan-card-chevron">' + iconHtml("chevron-down") + '</span>';
1221
+ el.appendChild(header);
1222
+ el.appendChild(body);
1223
+ ctx.addToMessages(el);
1224
+ currentPlanCardEl = el;
1225
+ } else {
1226
+ header = el.querySelector(".plan-card-header");
1227
+ body = el.querySelector(".plan-card-body");
1228
+ }
1205
1229
 
1206
- var body = document.createElement("div");
1207
- body.className = "plan-card-body";
1208
1230
  body.innerHTML = renderMarkdown(content);
1209
1231
  highlightCodeBlocks(body);
1210
1232
  renderMermaidBlocks(body);
1211
1233
 
1212
1234
  var copyBtn = header.querySelector(".plan-card-copy");
1213
1235
  if (copyBtn) {
1214
- copyBtn.addEventListener("click", function (e) {
1236
+ copyBtn.onclick = function (e) {
1215
1237
  e.stopPropagation();
1216
1238
  copyToClipboard(content).then(function () {
1217
1239
  copyBtn.innerHTML = iconHtml("check");
@@ -1221,18 +1243,11 @@ export function renderPlanCard(content) {
1221
1243
  refreshIcons();
1222
1244
  }, 1500);
1223
1245
  });
1224
- });
1246
+ };
1225
1247
  }
1226
1248
 
1227
- header.addEventListener("click", function () {
1228
- el.classList.toggle("collapsed");
1229
- });
1230
-
1231
- el.appendChild(header);
1232
- el.appendChild(body);
1233
- ctx.addToMessages(el);
1234
1249
  refreshIcons();
1235
- ctx.scrollToBottom();
1250
+ if (isNew) ctx.scrollToBottom();
1236
1251
  return el;
1237
1252
  }
1238
1253
 
@@ -1734,14 +1749,17 @@ function renderEditDiff(oldStr, newStr, filePath) {
1734
1749
 
1735
1750
  function isDiffContent(text) {
1736
1751
  var lines = text.split("\n");
1737
- var diffMarkers = 0;
1752
+ var hasHunkHeader = false;
1753
+ var hasPatchLine = false;
1738
1754
  for (var i = 0; i < Math.min(lines.length, 20); i++) {
1739
1755
  var l = lines[i];
1740
- if (l.startsWith("@@") || l.startsWith("---") || l.startsWith("+++")) {
1741
- diffMarkers++;
1756
+ if (l.startsWith("@@")) hasHunkHeader = true;
1757
+ if (l.startsWith("---") || l.startsWith("+++")) hasPatchLine = true;
1758
+ if ((l.startsWith("+") && !l.startsWith("+++")) || (l.startsWith("-") && !l.startsWith("---"))) {
1759
+ hasPatchLine = true;
1742
1760
  }
1743
1761
  }
1744
- return diffMarkers >= 2;
1762
+ return (hasHunkHeader && hasPatchLine) || hasPatchLine;
1745
1763
  }
1746
1764
 
1747
1765
  function getLanguageFromPath(filePath) {
@@ -2179,6 +2197,7 @@ export function saveToolState() {
2179
2197
  todoWidgetEl: todoWidgetEl,
2180
2198
  inPlanMode: inPlanMode,
2181
2199
  planContent: planContent,
2200
+ currentPlanCardEl: currentPlanCardEl,
2182
2201
  currentToolGroup: currentToolGroup,
2183
2202
  toolGroupCounter: toolGroupCounter,
2184
2203
  toolGroups: toolGroups,
@@ -2192,6 +2211,7 @@ export function restoreToolState(saved) {
2192
2211
  todoWidgetEl = saved.todoWidgetEl;
2193
2212
  inPlanMode = saved.inPlanMode;
2194
2213
  planContent = saved.planContent;
2214
+ currentPlanCardEl = saved.currentPlanCardEl || null;
2195
2215
  currentToolGroup = saved.currentToolGroup;
2196
2216
  toolGroupCounter = saved.toolGroupCounter;
2197
2217
  toolGroups = saved.toolGroups;
@@ -2207,6 +2227,7 @@ export function resetToolState() {
2207
2227
  thinkingGroup = null;
2208
2228
  inPlanMode = false;
2209
2229
  planContent = null;
2230
+ currentPlanCardEl = null;
2210
2231
  todoItems = [];
2211
2232
  todoWidgetEl = null;
2212
2233
  todoWidgetVisible = true;
package/lib/sdk-bridge.js CHANGED
@@ -120,6 +120,22 @@ function createSDKBridge(opts) {
120
120
  var clayTls = opts.clayTls || false;
121
121
  var clayAuthToken = opts.clayAuthToken || null;
122
122
  var onProcessingChanged = opts.onProcessingChanged || function () {};
123
+
124
+ function getModelsForVendor(vendor) {
125
+ if (vendor && sm.modelsByVendor && sm.modelsByVendor[vendor]) return sm.modelsByVendor[vendor];
126
+ return sm.availableModels || [];
127
+ }
128
+
129
+ function sendModelInfoForVendor(vendor, model) {
130
+ send({
131
+ type: "model_info",
132
+ model: model || "",
133
+ models: getModelsForVendor(vendor),
134
+ vendor: vendor || (adapter && adapter.vendor) || "claude",
135
+ availableVendors: sm.availableVendors || [],
136
+ installedVendors: sm.installedVendors || [],
137
+ });
138
+ }
123
139
  var onTurnDone = opts.onTurnDone || null;
124
140
 
125
141
  // --- Idle session reaper ---
@@ -648,7 +664,7 @@ function createSDKBridge(opts) {
648
664
  break;
649
665
  case "model_changed":
650
666
  sm.currentModel = metaData.model;
651
- send({ type: "model_info", model: metaData.model, models: sm.availableModels || [], vendor: adapter.vendor });
667
+ sendModelInfoForVendor(session.vendor || (adapter && adapter.vendor) || "claude", metaData.model);
652
668
  send({ type: "config_state", model: sm.currentModel, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
653
669
  break;
654
670
  case "effort_changed":
@@ -908,13 +924,49 @@ function createSDKBridge(opts) {
908
924
  }
909
925
 
910
926
  async function startQuery(session, text, images, linuxUser) {
927
+ async function ensureVendorReady(vendor) {
928
+ if (!vendor) return null;
929
+ var vendorAdapter = adapters[vendor] || null;
930
+ if (!vendorAdapter) {
931
+ var yoke = require("./yoke");
932
+ vendorAdapter = await yoke.lazyCreateAdapter(adapters, vendor, {
933
+ cwd: cwd,
934
+ dangerouslySkipPermissions: dangerouslySkipPermissions,
935
+ linuxUser: linuxUser || undefined,
936
+ clayPort: clayPort,
937
+ clayTls: clayTls,
938
+ clayAuthToken: clayAuthToken,
939
+ slug: slug,
940
+ });
941
+ } else if ((!sm.modelsByVendor || !sm.modelsByVendor[vendor]) && typeof vendorAdapter.init === "function") {
942
+ await vendorAdapter.init({
943
+ cwd: cwd,
944
+ dangerouslySkipPermissions: dangerouslySkipPermissions,
945
+ linuxUser: linuxUser || undefined,
946
+ clayPort: clayPort,
947
+ clayTls: clayTls,
948
+ clayAuthToken: clayAuthToken,
949
+ slug: slug,
950
+ });
951
+ }
952
+ if (vendorAdapter) {
953
+ sm.availableVendors = Object.keys(adapters);
954
+ sm.modelsByVendor = sm.modelsByVendor || {};
955
+ if (!sm.modelsByVendor[vendor] && typeof vendorAdapter.supportedModels === "function") {
956
+ sm.modelsByVendor[vendor] = await vendorAdapter.supportedModels();
957
+ }
958
+ }
959
+ return vendorAdapter;
960
+ }
961
+
911
962
  // If vendor is set but adapter not ready, try lazy creation (user may have logged in)
912
963
  if (session.vendor && !adapters[session.vendor]) {
913
- var yoke = require("./yoke");
914
- var lazyAdapter = yoke.lazyCreateAdapter(adapters, session.vendor, { cwd: cwd });
964
+ var lazyAdapter = await ensureVendorReady(session.vendor);
915
965
  if (lazyAdapter) {
916
966
  console.log("[sdk-bridge] Lazy adapter created for " + session.vendor);
917
967
  }
968
+ } else if (session.vendor) {
969
+ await ensureVendorReady(session.vendor);
918
970
  }
919
971
  // If still not available after lazy check, send auth_required
920
972
  if (session.vendor && !adapters[session.vendor]) {
@@ -1299,7 +1351,7 @@ function createSDKBridge(opts) {
1299
1351
  send({
1300
1352
  type: "model_info",
1301
1353
  model: sm.currentModel || "",
1302
- models: sm.availableModels || [],
1354
+ models: getModelsForVendor(defaultVendor),
1303
1355
  vendor: defaultVendor,
1304
1356
  availableVendors: sm.availableVendors,
1305
1357
  installedVendors: sm.installedVendors,
@@ -1311,7 +1363,7 @@ function createSDKBridge(opts) {
1311
1363
  // No active query — just store the model for next startQuery
1312
1364
  sm.currentModel = model;
1313
1365
  // Don't send vendor here: session vendor not yet bound, let client keep its selection
1314
- send({ type: "model_info", model: model, models: sm.availableModels || [] });
1366
+ sendModelInfoForVendor(null, model);
1315
1367
  send({ type: "config_state", model: sm.currentModel, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1316
1368
  return;
1317
1369
  }
@@ -1319,7 +1371,7 @@ function createSDKBridge(opts) {
1319
1371
  await session.queryInstance.setModel(model);
1320
1372
  sm.currentModel = model;
1321
1373
  var sessionVendor = session.vendor || (adapter && adapter.vendor) || "claude";
1322
- send({ type: "model_info", model: model, models: sm.availableModels || [], vendor: sessionVendor });
1374
+ sendModelInfoForVendor(sessionVendor, model);
1323
1375
  send({ type: "config_state", model: sm.currentModel, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1324
1376
  } catch (e) {
1325
1377
  send({ type: "error", text: "Failed to switch model: " + (e.message || e) });
@@ -36,6 +36,11 @@ function attachMessageProcessor(ctx) {
36
36
  sm.sendToSession(session, obj);
37
37
  }
38
38
 
39
+ function getModelsForVendor(vendor) {
40
+ if (vendor && sm.modelsByVendor && sm.modelsByVendor[vendor]) return sm.modelsByVendor[vendor];
41
+ return sm.availableModels || [];
42
+ }
43
+
39
44
  function toolActivityTextForSubagent(name, input) {
40
45
  if (name === "Bash" && input && input.description) return input.description;
41
46
  if (name === "Read" && input && input.file_path) return "Reading " + input.file_path.split("/").pop();
@@ -144,7 +149,15 @@ function attachMessageProcessor(ctx) {
144
149
  }
145
150
  if (parsed.model) {
146
151
  sm.currentModel = sm.currentModel || sm._savedDefaultModel || parsed.model;
147
- send({ type: "model_info", model: sm.currentModel, models: sm.availableModels || [] });
152
+ var initVendor = session.vendor || (adapter && adapter.vendor) || "claude";
153
+ send({
154
+ type: "model_info",
155
+ model: sm.currentModel,
156
+ models: getModelsForVendor(initVendor),
157
+ vendor: initVendor,
158
+ availableVendors: sm.availableVendors || [],
159
+ installedVendors: sm.installedVendors || [],
160
+ });
148
161
  }
149
162
  if (parsed.fastModeState) {
150
163
  sendAndRecord(session, { type: "fast_mode_state", state: parsed.fastModeState });
@@ -189,6 +202,47 @@ function attachMessageProcessor(ctx) {
189
202
  sendAndRecord(session, { type: "thinking_delta", text: parsed.text });
190
203
  }
191
204
 
205
+ } else if (parsed.yokeType === "tool_executing") {
206
+ sendAndRecord(session, {
207
+ type: "tool_executing",
208
+ id: parsed.toolId,
209
+ name: parsed.toolName,
210
+ input: parsed.input || {},
211
+ });
212
+
213
+ } else if (parsed.yokeType === "tool_result") {
214
+ sendAndRecord(session, {
215
+ type: "tool_result",
216
+ id: parsed.toolId,
217
+ content: parsed.content || "",
218
+ is_error: !!parsed.isError,
219
+ });
220
+
221
+ } else if (parsed.yokeType === "plan_updated") {
222
+ var todos = Array.isArray(parsed.plan) ? parsed.plan.map(function(step, idx) {
223
+ var todo = {
224
+ id: String(idx + 1),
225
+ content: step.step || "",
226
+ status: step.status || "pending",
227
+ };
228
+ if (todo.status === "in_progress" && parsed.explanation) {
229
+ todo.activeForm = parsed.explanation;
230
+ }
231
+ return todo;
232
+ }) : [];
233
+ sendAndRecord(session, {
234
+ type: "tool_executing",
235
+ id: parsed.turnId || "codex-plan",
236
+ name: "TodoWrite",
237
+ input: { todos: todos },
238
+ });
239
+
240
+ } else if (parsed.yokeType === "plan_content") {
241
+ sendAndRecord(session, {
242
+ type: "plan_content",
243
+ content: parsed.content || "",
244
+ });
245
+
192
246
  } else if (parsed.yokeType === "block_stop") {
193
247
  var idx = parsed.blockId;
194
248
  var block = session.blocks[idx];
package/lib/sessions.js CHANGED
@@ -95,6 +95,7 @@ function createSessionManager(opts) {
95
95
  if (session.ownerId) metaObj.ownerId = session.ownerId;
96
96
  if (session.vendor) metaObj.vendor = session.vendor;
97
97
  if (session.sessionVisibility) metaObj.sessionVisibility = session.sessionVisibility;
98
+ if (session.bookmarked) metaObj.bookmarked = true;
98
99
  if (session.lastRewindUuid) metaObj.lastRewindUuid = session.lastRewindUuid;
99
100
  if (session.loop) metaObj.loop = session.loop;
100
101
  if (session.debateState) metaObj.debateState = session.debateState;
@@ -200,6 +201,7 @@ function createSessionManager(opts) {
200
201
  if (m.debateSetupMode) session.debateSetupMode = true;
201
202
  if (m.ownerId) session.ownerId = m.ownerId;
202
203
  session.sessionVisibility = m.sessionVisibility || "shared";
204
+ session.bookmarked = !!m.bookmarked;
203
205
  sessions.set(localId, session);
204
206
  }
205
207
  }
@@ -238,6 +240,7 @@ function createSessionManager(opts) {
238
240
  loop: loop,
239
241
  ownerId: s.ownerId || null,
240
242
  sessionVisibility: s.sessionVisibility || "shared",
243
+ bookmarked: !!s.bookmarked,
241
244
  unread: unreadMap[s.localId] || 0,
242
245
  vendor: s.vendor || null,
243
246
  };
@@ -299,6 +302,7 @@ function createSessionManager(opts) {
299
302
  messageUUIDs: [],
300
303
  ownerId: (sessionOpts && sessionOpts.ownerId) || null,
301
304
  sessionVisibility: (sessionOpts && sessionOpts.sessionVisibility) || "shared",
305
+ bookmarked: false,
302
306
  vendor: (sessionOpts && sessionOpts.vendor) || null,
303
307
  };
304
308
  sessions.set(localId, session);
@@ -329,6 +333,7 @@ function createSessionManager(opts) {
329
333
  messageUUIDs: [],
330
334
  ownerId: (sessionOpts && sessionOpts.ownerId) || null,
331
335
  sessionVisibility: (sessionOpts && sessionOpts.sessionVisibility) || "shared",
336
+ bookmarked: false,
332
337
  vendor: (sessionOpts && sessionOpts.vendor) || null,
333
338
  };
334
339
  sessions.set(localId, session);
@@ -613,8 +618,10 @@ function createSessionManager(opts) {
613
618
  isProcessing: false,
614
619
  title: title,
615
620
  createdAt: Date.now(),
621
+ lastActivity: Date.now(),
616
622
  history: cliHistory,
617
623
  messageUUIDs: [],
624
+ bookmarked: false,
618
625
  };
619
626
  sessions.set(localId, session);
620
627
  saveSessionFile(session);
@@ -818,6 +825,14 @@ function createSessionManager(opts) {
818
825
  broadcastSessionList();
819
826
  return { ok: true };
820
827
  },
828
+ setSessionBookmarked: function (localId, bookmarked) {
829
+ var session = sessions.get(localId);
830
+ if (!session) return { error: "Session not found" };
831
+ session.bookmarked = !!bookmarked;
832
+ saveSessionFile(session);
833
+ broadcastSessionList();
834
+ return { ok: true };
835
+ },
821
836
  setSessionOwner: function (localId, ownerId) {
822
837
  var session = sessions.get(localId);
823
838
  if (!session) return { error: "Session not found" };
package/lib/ws-schema.js CHANGED
@@ -20,6 +20,7 @@ var schema = {
20
20
  "new_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Create a new blank session" },
21
21
  "delete_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Delete a session by ID" },
22
22
  "rename_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Rename a session" },
23
+ "set_session_bookmark": { direction: "c2s", handler: "lib/project-sessions.js", description: "Bookmark or unbookmark a session in the sidebar" },
23
24
  "resume_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Resume a CLI session by its CLI session ID" },
24
25
  "set_session_visibility": { direction: "c2s", handler: "lib/project-sessions.js", description: "Show or hide a session in the sidebar" },
25
26
  "search_sessions": { direction: "c2s", handler: "lib/project-sessions.js", description: "Search session titles" },