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.
@@ -139,6 +139,7 @@ function attachConnection(ctx) {
139
139
  loop: loop,
140
140
  ownerId: s.ownerId || null,
141
141
  sessionVisibility: s.sessionVisibility || "shared",
142
+ bookmarked: !!s.bookmarked,
142
143
  };
143
144
  }),
144
145
  });
@@ -124,6 +124,18 @@ function attachSessions(ctx) {
124
124
  return true;
125
125
  }
126
126
 
127
+ if (msg.type === "set_session_bookmark") {
128
+ if (typeof msg.sessionId === "number") {
129
+ var bookmarkTarget = sm.sessions.get(msg.sessionId);
130
+ if (!bookmarkTarget) return true;
131
+ if (usersModule.isMultiUser() && ws._clayUser) {
132
+ if (!usersModule.canAccessSession(ws._clayUser.id, bookmarkTarget, { visibility: "public" })) return true;
133
+ }
134
+ sm.setSessionBookmarked(msg.sessionId, !!msg.bookmarked);
135
+ }
136
+ return true;
137
+ }
138
+
127
139
  if (msg.type === "transfer_project_owner") {
128
140
  // Home directory projects: ownership is permanently locked
129
141
  if (osUsers && osUsers.length > 0 && /^\/home\/[^/]+\//.test(cwd)) {
package/lib/project.js CHANGED
@@ -779,11 +779,44 @@ function createProjectContext(opts) {
779
779
 
780
780
  // --- Vendor model switching ---
781
781
  if (msg.type === "get_vendor_models") {
782
- var vendorModels = (sm.modelsByVendor && sm.modelsByVendor[msg.vendor]) || [];
783
- var firstModel = vendorModels[0] || "";
784
- // model value can be string or {value, displayName} object
785
- var defaultModel = typeof firstModel === "string" ? firstModel : (firstModel.value || "");
786
- sendTo(ws, { type: "model_info", model: defaultModel, models: vendorModels, vendor: msg.vendor, availableVendors: sm.availableVendors || [], installedVendors: sm.installedVendors || [] });
782
+ (async function() {
783
+ if (msg.vendor) {
784
+ try {
785
+ var vendorAdapter = adapters[msg.vendor] || null;
786
+ if (!vendorAdapter) {
787
+ vendorAdapter = await yoke.lazyCreateAdapter(adapters, msg.vendor, {
788
+ cwd: cwd,
789
+ clayPort: serverPort,
790
+ clayTls: serverTls,
791
+ clayAuthToken: serverAuthToken,
792
+ slug: slug,
793
+ });
794
+ } else if ((!sm.modelsByVendor || !sm.modelsByVendor[msg.vendor]) && typeof vendorAdapter.init === "function") {
795
+ await vendorAdapter.init({
796
+ cwd: cwd,
797
+ clayPort: serverPort,
798
+ clayTls: serverTls,
799
+ clayAuthToken: serverAuthToken,
800
+ slug: slug,
801
+ });
802
+ }
803
+ if (vendorAdapter) {
804
+ sm.availableVendors = Object.keys(adapters);
805
+ sm.modelsByVendor = sm.modelsByVendor || {};
806
+ if (!sm.modelsByVendor[msg.vendor] && typeof vendorAdapter.supportedModels === "function") {
807
+ sm.modelsByVendor[msg.vendor] = await vendorAdapter.supportedModels();
808
+ }
809
+ }
810
+ } catch (e) {
811
+ console.error("[project] get_vendor_models lazy init failed for " + msg.vendor + ":", e.message || e);
812
+ }
813
+ }
814
+ var vendorModels = (sm.modelsByVendor && sm.modelsByVendor[msg.vendor]) || [];
815
+ var firstModel = vendorModels[0] || "";
816
+ // model value can be string or {value, displayName} object
817
+ var defaultModel = typeof firstModel === "string" ? firstModel : (firstModel.value || "");
818
+ sendTo(ws, { type: "model_info", model: defaultModel, models: vendorModels, vendor: msg.vendor, availableVendors: sm.availableVendors || [], installedVendors: sm.installedVendors || [] });
819
+ })();
787
820
  return;
788
821
  }
789
822
 
@@ -88,35 +88,6 @@
88
88
  height: 12px;
89
89
  }
90
90
 
91
- /* --- Debate pill button in mate sidebar (matches .new-ralph-pill) --- */
92
- .mate-debate-pill {
93
- display: inline-flex;
94
- align-items: center;
95
- justify-content: center;
96
- gap: 3px;
97
- height: 22px;
98
- padding: 0 8px;
99
- border: 1px solid var(--accent);
100
- border-radius: 6px;
101
- background: var(--accent-12, rgba(108, 92, 231, 0.12));
102
- color: var(--accent);
103
- font-size: 10px;
104
- font-weight: 700;
105
- font-family: inherit;
106
- letter-spacing: 0.2px;
107
- cursor: pointer;
108
- white-space: nowrap;
109
- transition: background 0.15s, color 0.15s;
110
- }
111
- .mate-debate-pill .lucide {
112
- width: 10px;
113
- height: 10px;
114
- }
115
- .mate-debate-pill:hover {
116
- background: var(--accent);
117
- color: #fff;
118
- }
119
-
120
91
  /* --- Debate header badges (inline with session title) --- */
121
92
  .debate-header-badge {
122
93
  display: inline-flex;
@@ -518,11 +518,28 @@
518
518
 
519
519
  .mobile-session-title {
520
520
  flex: 1;
521
+ display: inline-flex;
522
+ align-items: center;
523
+ gap: 6px;
521
524
  overflow: hidden;
522
525
  text-overflow: ellipsis;
523
526
  white-space: nowrap;
524
527
  }
525
528
 
529
+ .mobile-session-bookmark {
530
+ display: inline-flex;
531
+ align-items: center;
532
+ color: var(--accent, #ff7b54);
533
+ flex-shrink: 0;
534
+ }
535
+
536
+ .mobile-session-bookmark .lucide,
537
+ .mobile-session-bookmark svg {
538
+ width: 13px;
539
+ height: 13px;
540
+ display: block;
541
+ }
542
+
526
543
  .mobile-session-processing {
527
544
  width: 7px;
528
545
  height: 7px;
@@ -51,22 +51,27 @@
51
51
  align-items: center;
52
52
  gap: 4px;
53
53
  padding: 3px 10px;
54
- background: color-mix(in srgb, var(--bg-alt, #1a1a2e) 75%, transparent);
55
- backdrop-filter: blur(20px);
56
- -webkit-backdrop-filter: blur(20px);
57
- border: 1px solid var(--border, #333);
54
+ background:
55
+ linear-gradient(180deg, rgba(255,255,255,0.14), rgba(255,255,255,0.04)),
56
+ rgba(var(--overlay-rgb), 0.06);
57
+ backdrop-filter: blur(10px) saturate(1.1);
58
+ -webkit-backdrop-filter: blur(10px) saturate(1.1);
59
+ border: 1px solid rgba(255,255,255,0.18);
58
60
  border-radius: 999px;
59
61
  font-family: inherit;
60
62
  font-size: 11px;
61
63
  font-weight: 500;
62
64
  color: var(--text-muted);
63
65
  cursor: pointer;
64
- box-shadow: 0 4px 20px rgba(var(--shadow-rgb, 0,0,0), 0.2);
65
- transition: color 0.15s, background 0.15s;
66
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.18);
67
+ transition: color 0.15s, background 0.15s, border-color 0.15s;
66
68
  }
67
69
  .notif-banner-clear-all:hover {
68
70
  color: var(--text);
69
- background: color-mix(in srgb, var(--bg-alt, #1a1a2e) 90%, transparent);
71
+ background:
72
+ linear-gradient(180deg, rgba(255,255,255,0.18), rgba(255,255,255,0.06)),
73
+ rgba(var(--overlay-rgb), 0.08);
74
+ border-color: rgba(255,255,255,0.24);
70
75
  }
71
76
  .notif-banner-clear-all .lucide {
72
77
  width: 12px;
@@ -79,17 +84,19 @@
79
84
  align-items: center;
80
85
  gap: 10px;
81
86
  padding: 10px 14px;
82
- background: color-mix(in srgb, var(--bg-alt, #1a1a2e) 75%, transparent);
83
- backdrop-filter: blur(20px);
84
- -webkit-backdrop-filter: blur(20px);
85
- border: 1px solid var(--border, #333);
86
- border-radius: 12px;
87
- box-shadow: 0 4px 20px rgba(var(--shadow-rgb, 0,0,0), 0.25);
87
+ background:
88
+ linear-gradient(180deg, rgba(255,255,255,0.16), rgba(255,255,255,0.04)),
89
+ rgba(var(--overlay-rgb), 0.07);
90
+ backdrop-filter: blur(10px) saturate(1.08);
91
+ -webkit-backdrop-filter: blur(10px) saturate(1.08);
92
+ border: 1px solid rgba(255,255,255,0.2);
93
+ border-radius: 16px;
94
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.2);
88
95
  pointer-events: auto;
89
96
  cursor: pointer;
90
97
  opacity: 0;
91
98
  transform: translateY(-100%);
92
- transition: opacity 0.3s ease, transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
99
+ transition: opacity 0.3s ease, transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), border-color 0.18s ease, box-shadow 0.18s ease;
93
100
  }
94
101
  .notif-banner.show {
95
102
  opacity: 1;
@@ -99,6 +106,10 @@
99
106
  opacity: 0;
100
107
  transform: translateY(-100%);
101
108
  }
109
+ .notif-banner:hover {
110
+ border-color: rgba(255,255,255,0.28);
111
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.24);
112
+ }
102
113
 
103
114
  .notif-banner-icon {
104
115
  flex-shrink: 0;
@@ -107,9 +118,11 @@
107
118
  display: flex;
108
119
  align-items: center;
109
120
  justify-content: center;
110
- background: rgba(var(--overlay-rgb, 0,0,0), 0.06);
111
- border-radius: 8px;
121
+ background: linear-gradient(180deg, rgba(255,255,255,0.18), rgba(255,255,255,0.04));
122
+ border: 1px solid rgba(255,255,255,0.16);
123
+ border-radius: 10px;
112
124
  color: var(--text-muted);
125
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.18);
113
126
  }
114
127
  .notif-banner-icon .lucide { width: 16px; height: 16px; }
115
128
  .notif-banner-emoji { font-size: 18px; line-height: 1; }
@@ -215,7 +228,7 @@
215
228
  .notif-banner:hover .notif-banner-close { opacity: 1; }
216
229
  .notif-banner-close:hover {
217
230
  color: var(--text);
218
- background: rgba(var(--overlay-rgb, 0,0,0), 0.08);
231
+ background: rgba(255,255,255,0.12);
219
232
  }
220
233
  .notif-banner-close .lucide { width: 12px; height: 12px; }
221
234
 
@@ -46,6 +46,13 @@
46
46
  .usage-check-link:hover {
47
47
  background: color-mix(in srgb, var(--text-dimmer) 16%, transparent);
48
48
  }
49
+ .usage-check-vendor-icon {
50
+ width: 14px;
51
+ height: 14px;
52
+ border-radius: 4px;
53
+ object-fit: cover;
54
+ flex: 0 0 auto;
55
+ }
49
56
  .usage-check-link .lucide {
50
57
  width: 12px;
51
58
  height: 12px;
@@ -1258,4 +1265,3 @@ button.top-bar-pill.pill-accent:hover { background: color-mix(in srgb, var(--acc
1258
1265
  margin-top: 1.2rem;
1259
1266
  letter-spacing: 0.03em;
1260
1267
  }
1261
-
@@ -603,7 +603,7 @@
603
603
  gap: 2px;
604
604
  }
605
605
 
606
- .session-list-header-actions button:not(.new-ralph-pill) {
606
+ .session-list-header-actions button {
607
607
  display: flex;
608
608
  align-items: center;
609
609
  justify-content: center;
@@ -618,8 +618,8 @@
618
618
  transition: color 0.15s, background 0.15s;
619
619
  }
620
620
 
621
- .session-list-header-actions button:not(.new-ralph-pill) .lucide { width: 14px; height: 14px; }
622
- .session-list-header-actions button:not(.new-ralph-pill):hover { color: var(--text); background: var(--sidebar-hover); }
621
+ .session-list-header-actions button .lucide { width: 14px; height: 14px; }
622
+ .session-list-header-actions button:hover { color: var(--text); background: var(--sidebar-hover); }
623
623
 
624
624
  /* --- Session search --- */
625
625
  #session-search {
@@ -810,6 +810,64 @@
810
810
  box-shadow: 0 0 4px var(--success);
811
811
  }
812
812
 
813
+ .session-bookmark-btn {
814
+ display: flex;
815
+ width: 20px;
816
+ height: 20px;
817
+ border-radius: 6px;
818
+ border: none;
819
+ background: transparent;
820
+ color: var(--text-dimmer);
821
+ cursor: pointer;
822
+ align-items: center;
823
+ justify-content: center;
824
+ padding: 0;
825
+ margin-right: 2px;
826
+ flex-shrink: 0;
827
+ opacity: 0;
828
+ transition: opacity 0.2s ease, color 0.15s, background 0.15s;
829
+ }
830
+
831
+ .session-bookmark-btn.hover {
832
+ position: absolute;
833
+ right: 30px;
834
+ top: 50%;
835
+ transform: translateY(-50%);
836
+ }
837
+
838
+ .session-bookmark-btn.inline {
839
+ margin-right: 2px;
840
+ flex-shrink: 0;
841
+ }
842
+
843
+ .session-bookmark-btn .lucide,
844
+ .session-bookmark-btn svg {
845
+ width: 13px;
846
+ height: 13px;
847
+ display: block;
848
+ }
849
+
850
+ .session-item:hover .session-bookmark-btn.hover,
851
+ .session-item:focus-within .session-bookmark-btn.hover,
852
+ .session-bookmark-btn.bookmarked {
853
+ opacity: 1;
854
+ }
855
+
856
+ .session-bookmark-btn:hover {
857
+ color: var(--accent);
858
+ background: rgba(var(--overlay-rgb), 0.06);
859
+ }
860
+
861
+ .session-bookmark-btn.bookmarked {
862
+ color: var(--accent);
863
+ }
864
+
865
+ .session-bookmark-btn.bookmarked .lucide,
866
+ .session-bookmark-btn.bookmarked svg {
867
+ fill: currentColor;
868
+ stroke: currentColor;
869
+ }
870
+
813
871
  /* Session unread badge */
814
872
  .session-unread-badge {
815
873
  position: absolute;
@@ -832,6 +890,10 @@
832
890
  z-index: 2;
833
891
  }
834
892
 
893
+ .session-item .session-item-text {
894
+ padding-right: 28px;
895
+ }
896
+
835
897
  .session-unread-badge.has-unread {
836
898
  display: flex;
837
899
  }
@@ -1216,41 +1278,6 @@
1216
1278
  margin-left: 12px;
1217
1279
  }
1218
1280
 
1219
- /* --- New Ralph Loop pill button --- */
1220
- .new-ralph-pill {
1221
- display: inline-flex;
1222
- align-items: center;
1223
- justify-content: center;
1224
- gap: 3px;
1225
- height: 22px;
1226
- padding: 0 8px;
1227
- border: 1px solid var(--accent);
1228
- border-radius: 6px;
1229
- background: var(--accent-12, rgba(108, 92, 231, 0.12));
1230
- color: var(--accent);
1231
- font-size: 10px;
1232
- font-weight: 700;
1233
- font-family: inherit;
1234
- letter-spacing: 0.2px;
1235
- cursor: pointer;
1236
- white-space: nowrap;
1237
- transition: background 0.15s, color 0.15s;
1238
- }
1239
-
1240
- .new-ralph-pill .lucide {
1241
- width: 10px;
1242
- height: 10px;
1243
- }
1244
-
1245
- .new-ralph-pill + button {
1246
- margin-left: 4px;
1247
- }
1248
-
1249
- .new-ralph-pill:hover {
1250
- background: var(--accent);
1251
- color: #fff;
1252
- }
1253
-
1254
1281
  @media (hover: none) {
1255
1282
  .session-more-btn { opacity: 1; }
1256
1283
  .msg-copy-hint { opacity: 1; }
@@ -1719,4 +1746,3 @@
1719
1746
  50% { opacity: 0.18; }
1720
1747
  100% { opacity: 0.1; }
1721
1748
  }
1722
-
@@ -200,7 +200,6 @@
200
200
  <div class="session-list-header">
201
201
  <span>Sessions</span>
202
202
  <div class="session-list-header-actions">
203
- <button id="new-ralph-btn" type="button" class="new-ralph-pill" data-tip="New Loop"><i data-lucide="repeat"></i> Loop</button>
204
203
  <button id="new-session-btn" type="button" data-tip="New session"><i data-lucide="plus"></i></button>
205
204
  <button id="resume-session-btn" type="button" data-tip="Import CLI session"><i data-lucide="import"></i></button>
206
205
  <button id="search-session-btn" type="button" data-tip="Search sessions"><i data-lucide="search"></i></button>
@@ -270,7 +269,6 @@
270
269
  <div id="mate-sidebar-conversations">
271
270
  <div class="mate-sidebar-sessions-header">
272
271
  <span>Conversations</span>
273
- <button id="mate-debate-btn" class="mate-debate-pill" title="New Debate"><i data-lucide="mic"></i> Debate</button>
274
272
  <div class="mate-sidebar-actions">
275
273
  <button id="mate-search-session-btn" type="button" title="Search sessions"><i data-lucide="search"></i></button>
276
274
  <button id="mate-new-session-btn" type="button" title="New session"><i data-lucide="plus"></i></button>
@@ -337,7 +337,12 @@ export function processMessage(msg) {
337
337
  case "model_info": {
338
338
  var _modelVal = msg.model;
339
339
  if (_modelVal && typeof _modelVal === "object") _modelVal = _modelVal.value || _modelVal.displayName || "";
340
- var _miUpdate = { currentModel: _modelVal || store.get('currentModel'), currentModels: msg.models || [] };
340
+ var _miUpdate = { currentModels: msg.models || [] };
341
+ if (Object.prototype.hasOwnProperty.call(msg, "model")) {
342
+ _miUpdate.currentModel = _modelVal || "";
343
+ } else {
344
+ _miUpdate.currentModel = store.get('currentModel');
345
+ }
341
346
  if (msg.vendor) _miUpdate.currentVendor = msg.vendor;
342
347
  if (msg.availableVendors) _miUpdate.availableVendors = msg.availableVendors;
343
348
  if (msg.installedVendors) _miUpdate.installedVendors = msg.installedVendors;
@@ -573,6 +578,11 @@ export function processMessage(msg) {
573
578
  }
574
579
  break;
575
580
 
581
+ case "plan_content":
582
+ setPlanContent(msg.content || "");
583
+ renderPlanCard(msg.content || "");
584
+ break;
585
+
576
586
  case "context_preview":
577
587
  // Show a Context Card with tab screenshot between user message and assistant response
578
588
  if (msg.tab) {
@@ -410,7 +410,7 @@ export function initPanels() {
410
410
  if (vendor === (store.get('currentVendor') || "claude")) return;
411
411
  var installed = store.get('installedVendors') || [];
412
412
  if (installed.indexOf(vendor) === -1) return;
413
- store.set({ currentVendor: vendor });
413
+ store.set({ currentVendor: vendor, currentModel: "", currentModels: [] });
414
414
  var ws = getWs();
415
415
  if (ws) ws.send(JSON.stringify({ type: "set_vendor", vendor: vendor }));
416
416
  }
@@ -22,6 +22,23 @@ var fastModeIndicatorEl = null;
22
22
 
23
23
  // --- Internal helpers ---
24
24
 
25
+ function getVendorUsageMeta(vendor) {
26
+ if (vendor === "codex") {
27
+ return {
28
+ icon: "/codex-avatar.png",
29
+ alt: "Codex",
30
+ href: "https://chatgpt.com/admin/usage",
31
+ title: "Check usage on ChatGPT",
32
+ };
33
+ }
34
+ return {
35
+ icon: "/claude-code-avatar.png",
36
+ alt: "Claude Code",
37
+ href: "https://claude.ai/settings/usage",
38
+ title: "Check usage on claude.ai",
39
+ };
40
+ }
41
+
25
42
  function rateLimitTypeLabel(type) {
26
43
  if (!type) return "Usage";
27
44
  var labels = {
@@ -159,7 +176,13 @@ function tickRateLimitUsage() {
159
176
 
160
177
  // --- Exported functions ---
161
178
 
162
- export function initRateLimit() {}
179
+ export function initRateLimit() {
180
+ store.subscribe(function(state, prev) {
181
+ if (state.currentVendor !== prev.currentVendor && state.currentVendor && state.currentVendor !== "claude") {
182
+ clearScheduleDelay();
183
+ }
184
+ });
185
+ }
163
186
 
164
187
  export function handleRateLimitEvent(msg) {
165
188
  var isRejected = msg.status === "rejected";
@@ -180,7 +203,7 @@ export function handleRateLimitEvent(msg) {
180
203
  if (rateLimitResetTimer) clearTimeout(rateLimitResetTimer);
181
204
  // Auto-switch input to schedule mode: any message typed will be queued for after reset
182
205
  var delayUntilReset = msg.resetsAt - Date.now();
183
- if (delayUntilReset > 0) {
206
+ if (delayUntilReset > 0 && (store.get('currentVendor') || "claude") === "claude") {
184
207
  setScheduleDelayMs(delayUntilReset + 60000); // +1min buffer after reset
185
208
  }
186
209
  rateLimitResetTimer = setTimeout(function () {
@@ -210,11 +233,8 @@ export function updateRateLimitUsage(msg) {
210
233
  rateLimitUsageEl = document.createElement("a");
211
234
  rateLimitUsageEl.id = "rate-limit-usage-link";
212
235
  rateLimitUsageEl.className = "top-bar-pill pill-dim usage-check-link";
213
- var vendor = store.get('currentVendor') || "claude";
214
- rateLimitUsageEl.href = vendor === "codex" ? "https://chatgpt.com/admin/usage" : "https://claude.ai/settings/usage";
215
236
  rateLimitUsageEl.target = "_blank";
216
237
  rateLimitUsageEl.rel = "noopener";
217
- rateLimitUsageEl.title = vendor === "codex" ? "Check usage on ChatGPT" : "Check usage on claude.ai";
218
238
  var ref = document.getElementById("skip-perms-pill");
219
239
  topBarActions.insertBefore(rateLimitUsageEl, ref);
220
240
  }
@@ -231,7 +251,14 @@ export function updateRateLimitUsage(msg) {
231
251
  }
232
252
 
233
253
  var label = parts.length > 0 ? parts.join(" · ") : "Check usage";
234
- rateLimitUsageEl.innerHTML = iconHtml("activity") + '<span>' + label + '</span>' + iconHtml("external-link");
254
+ var vendor = store.get('currentVendor') || "claude";
255
+ var meta = getVendorUsageMeta(vendor);
256
+ rateLimitUsageEl.href = meta.href;
257
+ rateLimitUsageEl.title = meta.title;
258
+ rateLimitUsageEl.innerHTML =
259
+ '<img src="' + meta.icon + '" class="usage-check-vendor-icon" alt="' + meta.alt + '">' +
260
+ '<span>' + label + '</span>' +
261
+ iconHtml("external-link");
235
262
  refreshIcons();
236
263
 
237
264
  // Start or stop live countdown tick
@@ -389,7 +389,13 @@ function createMobileSessionItem(s) {
389
389
 
390
390
  var titleSpan = document.createElement("span");
391
391
  titleSpan.className = "mobile-session-title";
392
- titleSpan.textContent = s.title || "New Session";
392
+ if (s.bookmarked) {
393
+ var bookmarkIcon = document.createElement("span");
394
+ bookmarkIcon.className = "mobile-session-bookmark";
395
+ bookmarkIcon.innerHTML = iconHtml("star");
396
+ titleSpan.appendChild(bookmarkIcon);
397
+ }
398
+ titleSpan.appendChild(document.createTextNode(s.title || "New Session"));
393
399
  el.appendChild(titleSpan);
394
400
 
395
401
  // Unread badge (right side)
@@ -758,12 +764,37 @@ function renderMobileSessionsInto(container) {
758
764
 
759
765
  // Sort by lastActivity descending
760
766
  items.sort(function (a, b) {
767
+ var aBookmarked = !!(a.type === "loop" ? false : a.data && a.data.bookmarked);
768
+ var bBookmarked = !!(b.type === "loop" ? false : b.data && b.data.bookmarked);
769
+ if (aBookmarked !== bBookmarked) return aBookmarked ? -1 : 1;
761
770
  return (b.lastActivity || 0) - (a.lastActivity || 0);
762
771
  });
763
772
 
764
- var currentGroup = "";
773
+ var bookmarkedItems = [];
774
+ var regularItems = [];
765
775
  for (var n = 0; n < items.length; n++) {
766
776
  var item = items[n];
777
+ if (item.type === "session" && item.data && item.data.bookmarked) {
778
+ bookmarkedItems.push(item);
779
+ } else {
780
+ regularItems.push(item);
781
+ }
782
+ }
783
+
784
+ if (bookmarkedItems.length > 0) {
785
+ var bookmarkedHeader = document.createElement("div");
786
+ bookmarkedHeader.className = "mobile-sheet-group";
787
+ bookmarkedHeader.textContent = "Bookmarked";
788
+ container.appendChild(bookmarkedHeader);
789
+
790
+ for (var bi = 0; bi < bookmarkedItems.length; bi++) {
791
+ container.appendChild(createMobileSessionItem(bookmarkedItems[bi].data));
792
+ }
793
+ }
794
+
795
+ var currentGroup = "";
796
+ for (var ri = 0; ri < regularItems.length; ri++) {
797
+ var item = regularItems[ri];
767
798
  var group = getDateGroup(item.lastActivity || 0);
768
799
  if (group !== currentGroup) {
769
800
  currentGroup = group;
@@ -929,6 +960,7 @@ function renderSheetSearch(listEl) {
929
960
  function renderSearchResults(container, query) {
930
961
  container.innerHTML = "";
931
962
  var sorted = getCachedSessions().slice().sort(function (a, b) {
963
+ if (!!a.bookmarked !== !!b.bookmarked) return a.bookmarked ? -1 : 1;
932
964
  return (b.lastActivity || 0) - (a.lastActivity || 0);
933
965
  });
934
966
 
@@ -945,7 +977,13 @@ function renderSearchResults(container, query) {
945
977
 
946
978
  var titleSpan = document.createElement("span");
947
979
  titleSpan.className = "mobile-session-title";
948
- titleSpan.textContent = title;
980
+ if (s.bookmarked) {
981
+ var bookmarkIcon = document.createElement("span");
982
+ bookmarkIcon.className = "mobile-session-bookmark";
983
+ bookmarkIcon.innerHTML = iconHtml("star");
984
+ titleSpan.appendChild(bookmarkIcon);
985
+ }
986
+ titleSpan.appendChild(document.createTextNode(title));
949
987
  el.appendChild(titleSpan);
950
988
 
951
989
  if (s.isProcessing) {