clay-server 2.34.0-beta.9 → 2.34.1-beta.1

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.
@@ -94,12 +94,14 @@ export function initSidebar(_ctx) {
94
94
  }
95
95
  } catch (e) {}
96
96
 
97
- ctx.newSessionBtn.addEventListener("click", function () {
98
- if (ctx.ws && ctx.connected) {
99
- ctx.ws.send(JSON.stringify({ type: "new_session" }));
100
- closeSidebar();
101
- }
102
- });
97
+ if (ctx.newSessionBtn) {
98
+ ctx.newSessionBtn.addEventListener("click", function () {
99
+ if (ctx.ws && ctx.connected) {
100
+ ctx.ws.send(JSON.stringify({ type: "new_session" }));
101
+ closeSidebar();
102
+ }
103
+ });
104
+ }
103
105
 
104
106
  // --- Loop (Ralph wizard) tool-palette tile ---
105
107
  // The tile is rendered by tool-palette.js at the stable id
@@ -1,7 +1,7 @@
1
1
  import { escapeHtml, copyToClipboard } from './utils.js';
2
2
  import { iconHtml, refreshIcons } from './icons.js';
3
3
  import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks } from './markdown.js';
4
- import { renderUnifiedDiff, renderSplitDiff, renderPatchDiff } from './diff.js';
4
+ import { renderUnifiedDiff, renderSplitDiff, renderPatchDiff, reconstructPatchSources } from './diff.js';
5
5
  import { openFile } from './filebrowser.js';
6
6
  import { mateAvatarUrl } from './avatar.js';
7
7
  import { getChatLayout } from './theme.js';
@@ -1740,12 +1740,12 @@ export function updateToolExecuting(id, name, input) {
1740
1740
  ctx.scrollToBottom();
1741
1741
  }
1742
1742
 
1743
- function renderEditDiff(oldStr, newStr, filePath) {
1743
+ // Shared chrome (filename header + unified/split toggle) for diff renderings.
1744
+ // makeUnified and makeSplit are factories that return a fresh body element.
1745
+ function buildDiffChrome(filePath, linkOldStr, linkNewStr, makeUnified, makeSplit) {
1744
1746
  var wrapper = document.createElement("div");
1745
1747
  wrapper.className = "edit-diff";
1746
- var lang = getLanguageFromPath(filePath);
1747
1748
 
1748
- // Header with file path and split toggle (desktop only)
1749
1749
  var header = document.createElement("div");
1750
1750
  header.className = "edit-diff-header";
1751
1751
 
@@ -1758,7 +1758,7 @@ function renderEditDiff(oldStr, newStr, filePath) {
1758
1758
  e.stopPropagation();
1759
1759
  openFile(fp, { diff: { oldStr: os || "", newStr: ns || "" } });
1760
1760
  });
1761
- })(filePath, oldStr, newStr);
1761
+ })(filePath, linkOldStr, linkNewStr);
1762
1762
  }
1763
1763
  header.appendChild(pathSpan);
1764
1764
 
@@ -1784,7 +1784,7 @@ function renderEditDiff(oldStr, newStr, filePath) {
1784
1784
 
1785
1785
  wrapper.appendChild(header);
1786
1786
 
1787
- var currentBody = renderUnifiedDiff(oldStr, newStr, lang);
1787
+ var currentBody = makeUnified();
1788
1788
  wrapper.appendChild(currentBody);
1789
1789
 
1790
1790
  unifiedBtn.addEventListener("click", function (e) {
@@ -1794,7 +1794,7 @@ function renderEditDiff(oldStr, newStr, filePath) {
1794
1794
  unifiedBtn.classList.add("active");
1795
1795
  splitBtn.classList.remove("active");
1796
1796
  wrapper.removeChild(currentBody);
1797
- currentBody = renderUnifiedDiff(oldStr, newStr, lang);
1797
+ currentBody = makeUnified();
1798
1798
  wrapper.appendChild(currentBody);
1799
1799
  refreshIcons();
1800
1800
  });
@@ -1806,7 +1806,7 @@ function renderEditDiff(oldStr, newStr, filePath) {
1806
1806
  splitBtn.classList.add("active");
1807
1807
  unifiedBtn.classList.remove("active");
1808
1808
  wrapper.removeChild(currentBody);
1809
- currentBody = renderSplitDiff(oldStr, newStr, lang);
1809
+ currentBody = makeSplit();
1810
1810
  wrapper.appendChild(currentBody);
1811
1811
  refreshIcons();
1812
1812
  });
@@ -1814,6 +1814,29 @@ function renderEditDiff(oldStr, newStr, filePath) {
1814
1814
  return wrapper;
1815
1815
  }
1816
1816
 
1817
+ function renderEditDiff(oldStr, newStr, filePath) {
1818
+ var lang = getLanguageFromPath(filePath);
1819
+ return buildDiffChrome(
1820
+ filePath,
1821
+ oldStr,
1822
+ newStr,
1823
+ function () { return renderUnifiedDiff(oldStr, newStr, lang); },
1824
+ function () { return renderSplitDiff(oldStr, newStr, lang); }
1825
+ );
1826
+ }
1827
+
1828
+ function renderPatchDiffBlock(patchText, filePath) {
1829
+ var lang = getLanguageFromPath(filePath);
1830
+ var sources = reconstructPatchSources(patchText);
1831
+ return buildDiffChrome(
1832
+ filePath,
1833
+ sources.oldStr,
1834
+ sources.newStr,
1835
+ function () { return renderPatchDiff(patchText, lang); },
1836
+ function () { return renderSplitDiff(sources.oldStr, sources.newStr, lang); }
1837
+ );
1838
+ }
1839
+
1817
1840
  function isDiffContent(text) {
1818
1841
  var lines = text.split("\n");
1819
1842
  var hasHunkHeader = false;
@@ -1921,8 +1944,12 @@ export function updateToolResult(id, content, isError, images) {
1921
1944
  if (hasEditDiff) {
1922
1945
  resultBlock.appendChild(renderEditDiff(tool.input.old_string, tool.input.new_string, tool.input.file_path));
1923
1946
  } else if (!isError && isDiffContent(displayContent)) {
1924
- var patchLang = tool.input && tool.input.file_path ? getLanguageFromPath(tool.input.file_path) : null;
1925
- resultBlock.appendChild(renderPatchDiff(displayContent, patchLang));
1947
+ var patchFilePath = tool.input && tool.input.file_path ? tool.input.file_path : null;
1948
+ if (patchFilePath) {
1949
+ resultBlock.appendChild(renderPatchDiffBlock(displayContent, patchFilePath));
1950
+ } else {
1951
+ resultBlock.appendChild(renderPatchDiff(displayContent, null));
1952
+ }
1926
1953
  } else if (!isError && tool.name === "Read" && tool.input && tool.input.file_path && isImagePath(tool.input.file_path)) {
1927
1954
  // Image file: show inline preview
1928
1955
  var imgWrap = document.createElement("div");
package/lib/public/sw.js CHANGED
@@ -1,4 +1,4 @@
1
- var CACHE_NAME = "clay-offline-v2";
1
+ var CACHE_NAME = "clay-offline-v3";
2
2
 
3
3
  self.addEventListener("install", function (event) {
4
4
  event.waitUntil(self.skipWaiting());
package/lib/sdk-bridge.js CHANGED
@@ -182,6 +182,22 @@ function createSDKBridge(opts) {
182
182
  return sm.availableModels || [];
183
183
  }
184
184
 
185
+ // Model list entries may be plain strings (Codex) or { value, displayName }
186
+ // objects (Claude SDK). Normalize to the identifier string.
187
+ function modelEntryValue(entry) {
188
+ if (!entry) return "";
189
+ if (typeof entry === "string") return entry;
190
+ return entry.value || entry.id || "";
191
+ }
192
+
193
+ function modelListContains(list, modelId) {
194
+ if (!list || !modelId) return false;
195
+ for (var mi = 0; mi < list.length; mi++) {
196
+ if (modelEntryValue(list[mi]) === modelId) return true;
197
+ }
198
+ return false;
199
+ }
200
+
185
201
  function sendModelInfoForVendor(vendor, model) {
186
202
  send({
187
203
  type: "model_info",
@@ -1221,10 +1237,15 @@ function createSDKBridge(opts) {
1221
1237
  var sessionVendor = session.vendor || (adapter && adapter.vendor) || null;
1222
1238
  if (sessionVendor) {
1223
1239
  var vendorModels = (sm.modelsByVendor && sm.modelsByVendor[sessionVendor]) || [];
1224
- if (vendorModels.length > 0 && queryModel && vendorModels.indexOf(queryModel) === -1) {
1225
- queryModel = vendorModels[0];
1240
+ if (vendorModels.length > 0 && queryModel && !modelListContains(vendorModels, queryModel)) {
1241
+ queryModel = modelEntryValue(vendorModels[0]);
1226
1242
  }
1227
1243
  }
1244
+ // Guard against anything upstream having set queryModel to an object
1245
+ // (e.g. a cached ModelInfo leaked through). Always coerce to string id.
1246
+ if (queryModel && typeof queryModel !== "string") {
1247
+ queryModel = modelEntryValue(queryModel) || undefined;
1248
+ }
1228
1249
 
1229
1250
  var codexConfig = getCodexConfig(sm);
1230
1251
  var mergedMcpServers = mergeMcpServers(getMcpServers(), getRemoteMcpServers) || undefined;
@@ -1480,6 +1501,10 @@ function createSDKBridge(opts) {
1480
1501
  }
1481
1502
 
1482
1503
  async function setModel(session, model) {
1504
+ // Normalize to string id in case a { value, displayName } object slips in
1505
+ if (model && typeof model !== "string") {
1506
+ model = modelEntryValue(model);
1507
+ }
1483
1508
  if (!session.queryInstance) {
1484
1509
  // No active query — just store the model for next startQuery
1485
1510
  sm.currentModel = model;
package/lib/sessions.js CHANGED
@@ -96,6 +96,7 @@ function createSessionManager(opts) {
96
96
  if (session.vendor) metaObj.vendor = session.vendor;
97
97
  if (session.sessionVisibility) metaObj.sessionVisibility = session.sessionVisibility;
98
98
  if (session.bookmarked) metaObj.bookmarked = true;
99
+ if (typeof session.favoriteOrder === "number") metaObj.favoriteOrder = session.favoriteOrder;
99
100
  if (session.lastRewindUuid) metaObj.lastRewindUuid = session.lastRewindUuid;
100
101
  if (session.loop) metaObj.loop = session.loop;
101
102
  if (session.debateState) metaObj.debateState = session.debateState;
@@ -202,6 +203,7 @@ function createSessionManager(opts) {
202
203
  if (m.ownerId) session.ownerId = m.ownerId;
203
204
  session.sessionVisibility = m.sessionVisibility || "shared";
204
205
  session.bookmarked = !!m.bookmarked;
206
+ session.favoriteOrder = typeof m.favoriteOrder === "number" ? m.favoriteOrder : null;
205
207
  sessions.set(localId, session);
206
208
  }
207
209
  }
@@ -241,6 +243,7 @@ function createSessionManager(opts) {
241
243
  ownerId: s.ownerId || null,
242
244
  sessionVisibility: s.sessionVisibility || "shared",
243
245
  bookmarked: !!s.bookmarked,
246
+ favoriteOrder: typeof s.favoriteOrder === "number" ? s.favoriteOrder : null,
244
247
  unread: unreadMap[s.localId] || 0,
245
248
  vendor: s.vendor || null,
246
249
  };
@@ -303,6 +306,7 @@ function createSessionManager(opts) {
303
306
  ownerId: (sessionOpts && sessionOpts.ownerId) || null,
304
307
  sessionVisibility: (sessionOpts && sessionOpts.sessionVisibility) || "shared",
305
308
  bookmarked: false,
309
+ favoriteOrder: null,
306
310
  vendor: (sessionOpts && sessionOpts.vendor) || null,
307
311
  };
308
312
  sessions.set(localId, session);
@@ -334,6 +338,7 @@ function createSessionManager(opts) {
334
338
  ownerId: (sessionOpts && sessionOpts.ownerId) || null,
335
339
  sessionVisibility: (sessionOpts && sessionOpts.sessionVisibility) || "shared",
336
340
  bookmarked: false,
341
+ favoriteOrder: null,
337
342
  vendor: (sessionOpts && sessionOpts.vendor) || null,
338
343
  };
339
344
  sessions.set(localId, session);
@@ -525,6 +530,38 @@ function createSessionManager(opts) {
525
530
  sessions.delete(localId);
526
531
  }
527
532
 
533
+ function deleteSessionsBulk(localIds, targetWs) {
534
+ if (!Array.isArray(localIds) || localIds.length === 0) return;
535
+
536
+ var seen = {};
537
+ var ids = [];
538
+ for (var i = 0; i < localIds.length; i++) {
539
+ var id = localIds[i];
540
+ if (typeof id !== "number" || seen[id] || !sessions.has(id)) continue;
541
+ seen[id] = true;
542
+ ids.push(id);
543
+ }
544
+ if (ids.length === 0) return;
545
+
546
+ var deletedActive = false;
547
+ for (var j = 0; j < ids.length; j++) {
548
+ if (ids[j] === activeSessionId) deletedActive = true;
549
+ deleteSessionQuiet(ids[j]);
550
+ }
551
+
552
+ if (sessions.size === 0) {
553
+ createSession(null, targetWs);
554
+ return;
555
+ }
556
+
557
+ if (deletedActive) {
558
+ var remaining = [...sessions.keys()];
559
+ switchSession(remaining[remaining.length - 1], targetWs);
560
+ } else {
561
+ broadcastSessionList();
562
+ }
563
+ }
564
+
528
565
  function doSendToSession(session, obj) {
529
566
  // Send to active clients without recording to history/disk (ephemeral data)
530
567
  if (sendEach) {
@@ -622,7 +659,10 @@ function createSessionManager(opts) {
622
659
  history: cliHistory,
623
660
  messageUUIDs: [],
624
661
  bookmarked: false,
662
+ favoriteOrder: null,
625
663
  };
664
+ if (opts && opts.vendor) session.vendor = opts.vendor;
665
+ if (opts && opts.ownerId) session.ownerId = opts.ownerId;
626
666
  sessions.set(localId, session);
627
667
  saveSessionFile(session);
628
668
  switchSession(localId, targetWs);
@@ -843,6 +883,7 @@ function createSessionManager(opts) {
843
883
  switchSession: switchSession,
844
884
  deleteSession: deleteSession,
845
885
  deleteSessionQuiet: deleteSessionQuiet,
886
+ deleteSessionsBulk: deleteSessionsBulk,
846
887
  resumeSession: resumeSession,
847
888
  broadcastSessionList: broadcastSessionList,
848
889
  getTotalUnread: function (ws) {
@@ -876,10 +917,61 @@ function createSessionManager(opts) {
876
917
  var session = sessions.get(localId);
877
918
  if (!session) return { error: "Session not found" };
878
919
  session.bookmarked = !!bookmarked;
920
+ if (session.bookmarked) {
921
+ var maxOrder = -1;
922
+ sessions.forEach(function (s) {
923
+ if (s.bookmarked && typeof s.favoriteOrder === "number" && s.favoriteOrder > maxOrder) {
924
+ maxOrder = s.favoriteOrder;
925
+ }
926
+ });
927
+ session.favoriteOrder = maxOrder + 1;
928
+ } else {
929
+ session.favoriteOrder = null;
930
+ }
879
931
  saveSessionFile(session);
880
932
  broadcastSessionList();
881
933
  return { ok: true };
882
934
  },
935
+ reorderBookmarkedSessions: function (sourceId, targetId, insertBefore) {
936
+ var source = sessions.get(sourceId);
937
+ var target = sessions.get(targetId);
938
+ if (!source || !target) return { error: "Session not found" };
939
+ if (!source.bookmarked || !target.bookmarked) return { error: "Only favorites can be reordered" };
940
+
941
+ var favorites = [];
942
+ sessions.forEach(function (s) {
943
+ if (s.bookmarked) favorites.push(s);
944
+ });
945
+ favorites.sort(function (a, b) {
946
+ var ao = typeof a.favoriteOrder === "number" ? a.favoriteOrder : Number.MAX_SAFE_INTEGER;
947
+ var bo = typeof b.favoriteOrder === "number" ? b.favoriteOrder : Number.MAX_SAFE_INTEGER;
948
+ if (ao !== bo) return ao - bo;
949
+ return (b.lastActivity || 0) - (a.lastActivity || 0);
950
+ });
951
+
952
+ var reordered = [];
953
+ for (var i = 0; i < favorites.length; i++) {
954
+ if (favorites[i].localId !== sourceId) reordered.push(favorites[i]);
955
+ }
956
+
957
+ var targetIdx = -1;
958
+ for (var j = 0; j < reordered.length; j++) {
959
+ if (reordered[j].localId === targetId) {
960
+ targetIdx = j;
961
+ break;
962
+ }
963
+ }
964
+ if (targetIdx === -1) return { error: "Target favorite not found" };
965
+ if (!insertBefore) targetIdx++;
966
+ reordered.splice(targetIdx, 0, source);
967
+
968
+ for (var k = 0; k < reordered.length; k++) {
969
+ reordered[k].favoriteOrder = k;
970
+ saveSessionFile(reordered[k]);
971
+ }
972
+ broadcastSessionList();
973
+ return { ok: true };
974
+ },
883
975
  setSessionOwner: function (localId, ownerId) {
884
976
  var session = sessions.get(localId);
885
977
  if (!session) return { error: "Session not found" };
package/lib/ws-schema.js CHANGED
@@ -21,6 +21,8 @@ var schema = {
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
23
  "set_session_bookmark": { direction: "c2s", handler: "lib/project-sessions.js", description: "Bookmark or unbookmark a session in the sidebar" },
24
+ "reorder_session_bookmarks": { direction: "c2s", handler: "lib/project-sessions.js", description: "Reorder favorited sessions within the favorites area" },
25
+ "bulk_delete_sessions": { direction: "c2s", handler: "lib/project-sessions.js", description: "Delete a group of sessions at once" },
24
26
  "resume_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Resume a CLI session by its CLI session ID" },
25
27
  "set_session_visibility": { direction: "c2s", handler: "lib/project-sessions.js", description: "Show or hide a session in the sidebar" },
26
28
  "search_sessions": { direction: "c2s", handler: "lib/project-sessions.js", description: "Search session titles" },
@@ -793,10 +793,37 @@ function createCodexQueryHandle(appServer, queryOpts) {
793
793
 
794
794
  // --- Main query loop ---
795
795
  async function runQueryLoop(initialMessage) {
796
- // Prepend system prompt (project instructions from YOKE layer) to first message
797
- var currentMessage = systemPrompt
798
- ? systemPrompt + "\n\n" + initialMessage
799
- : initialMessage;
796
+ // Prepend system prompt (project instructions from YOKE layer) to first message.
797
+ // initialMessage may be a string (text-only) or an array of content items
798
+ // (e.g. [{ type: "text", text: "..." }, ...] when images/attachments are present).
799
+ // Naive string concatenation on an array coerces it via toString(), producing
800
+ // "[object Object]" inside the prompt, so we must branch on the shape.
801
+ var currentMessage;
802
+ if (!systemPrompt) {
803
+ currentMessage = initialMessage;
804
+ } else if (typeof initialMessage === "string") {
805
+ currentMessage = systemPrompt + "\n\n" + initialMessage;
806
+ } else if (Array.isArray(initialMessage)) {
807
+ // Prepend systemPrompt to the first text item; if none exists, insert one.
808
+ var cloned = initialMessage.slice();
809
+ var injected = false;
810
+ for (var i = 0; i < cloned.length; i++) {
811
+ if (cloned[i] && cloned[i].type === "text") {
812
+ cloned[i] = {
813
+ type: "text",
814
+ text: systemPrompt + "\n\n" + (cloned[i].text || ""),
815
+ };
816
+ injected = true;
817
+ break;
818
+ }
819
+ }
820
+ if (!injected) {
821
+ cloned.unshift({ type: "text", text: systemPrompt });
822
+ }
823
+ currentMessage = cloned;
824
+ } else {
825
+ currentMessage = initialMessage;
826
+ }
800
827
 
801
828
  try {
802
829
  // Set event handler on app-server
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.34.0-beta.9",
3
+ "version": "2.34.1-beta.1",
4
4
  "description": "Self-hosted Claude Code in your browser. Multi-session, multi-user, push notifications.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",