clay-server 2.19.0 → 2.20.0-beta.2

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.
Files changed (41) hide show
  1. package/README.md +51 -91
  2. package/bin/cli.js +49 -14
  3. package/lib/builtin-mates.js +360 -0
  4. package/lib/cli-sessions.js +3 -3
  5. package/lib/config.js +30 -4
  6. package/lib/daemon.js +28 -5
  7. package/lib/mates.js +169 -7
  8. package/lib/notes.js +20 -0
  9. package/lib/os-users.js +71 -2
  10. package/lib/project.js +903 -228
  11. package/lib/public/app.js +249 -69
  12. package/lib/public/css/icon-strip.css +55 -2
  13. package/lib/public/css/input.css +50 -30
  14. package/lib/public/css/mates.css +316 -2
  15. package/lib/public/css/mention.css +23 -0
  16. package/lib/public/css/mobile-nav.css +198 -0
  17. package/lib/public/css/overlays.css +23 -0
  18. package/lib/public/css/rewind.css +32 -0
  19. package/lib/public/css/title-bar.css +3 -0
  20. package/lib/public/css/user-settings.css +2 -2
  21. package/lib/public/index.html +65 -14
  22. package/lib/public/mates/ally.png +0 -0
  23. package/lib/public/mates/sage.jpg +0 -0
  24. package/lib/public/mates/scout.png +0 -0
  25. package/lib/public/modules/command-palette.js +44 -4
  26. package/lib/public/modules/filebrowser.js +2 -0
  27. package/lib/public/modules/input.js +158 -16
  28. package/lib/public/modules/mate-knowledge.js +2 -0
  29. package/lib/public/modules/mate-memory.js +353 -0
  30. package/lib/public/modules/mention.js +77 -2
  31. package/lib/public/modules/notifications.js +0 -8
  32. package/lib/public/modules/server-settings.js +11 -4
  33. package/lib/public/modules/sidebar.js +507 -21
  34. package/lib/public/modules/theme.js +10 -12
  35. package/lib/public/modules/tools.js +84 -12
  36. package/lib/public/modules/user-settings.js +45 -12
  37. package/lib/sdk-bridge.js +122 -61
  38. package/lib/server.js +209 -13
  39. package/lib/sessions.js +4 -4
  40. package/lib/users.js +89 -0
  41. package/package.json +1 -1
@@ -20,6 +20,8 @@ var searchDebounce = null;
20
20
  var cachedSessions = [];
21
21
  var expandedLoopGroups = new Set();
22
22
  var expandedLoopRuns = new Set();
23
+ var expandedMobileLoopGroups = new Set();
24
+ var expandedMobileLoopRuns = new Set();
23
25
 
24
26
  // --- Cached project data for mobile sheet ---
25
27
  var cachedProjectList = [];
@@ -959,7 +961,19 @@ function renderSheetSessions(listEl) {
959
961
  })(cachedProjectList[ci]);
960
962
  }
961
963
 
962
- for (var mi = 0; mi < cachedMates.length; mi++) {
964
+ var favoriteChipMates = cachedMates.filter(function (m) {
965
+ if (cachedDmRemovedUsers[m.id]) return false;
966
+ if (cachedDmFavorites.indexOf(m.id) !== -1) return true;
967
+ if (cachedDmUnread[m.id] && cachedDmUnread[m.id] > 0) return true;
968
+ return false;
969
+ });
970
+ var sortedChipMates = favoriteChipMates.sort(function (a, b) {
971
+ var aBuiltin = a.builtinKey ? 1 : 0;
972
+ var bBuiltin = b.builtinKey ? 1 : 0;
973
+ if (aBuiltin !== bBuiltin) return bBuiltin - aBuiltin;
974
+ return (a.createdAt || 0) - (b.createdAt || 0);
975
+ });
976
+ for (var mi = 0; mi < sortedChipMates.length; mi++) {
963
977
  (function (mate) {
964
978
  var mp = mate.profile || {};
965
979
  var chip = document.createElement("button");
@@ -998,7 +1012,7 @@ function renderSheetSessions(listEl) {
998
1012
  }
999
1013
 
1000
1014
  chips.push(chip);
1001
- })(cachedMates[mi]);
1015
+ })(sortedChipMates[mi]);
1002
1016
  }
1003
1017
 
1004
1018
  for (var i = 0; i < chips.length; i++) {
@@ -1104,7 +1118,242 @@ function createMobileSessionItem(s) {
1104
1118
  return el;
1105
1119
  }
1106
1120
 
1107
- // Helper: render sorted sessions into a container with date groups
1121
+ // Helper: create a mobile loop child element (individual session inside a group)
1122
+ function createMobileLoopChild(s) {
1123
+ var el = document.createElement("button");
1124
+ el.className = "mobile-loop-child" + (s.active ? " active" : "");
1125
+
1126
+ if (s.isProcessing) {
1127
+ var dot = document.createElement("span");
1128
+ dot.className = "mobile-session-processing";
1129
+ el.appendChild(dot);
1130
+ }
1131
+
1132
+ var textSpan = document.createElement("span");
1133
+ textSpan.className = "mobile-session-title";
1134
+ if (s.loop) {
1135
+ var isRalphChild = s.loop.source === "ralph";
1136
+ var roleName = s.loop.role === "crafting" ? "Crafting" : s.loop.role === "judge" ? "Judge" : (isRalphChild ? "Coder" : "Run");
1137
+ var iterSuffix = s.loop.role === "crafting" ? "" : " #" + s.loop.iteration;
1138
+ var roleCls = s.loop.role === "crafting" ? " crafting" : (!isRalphChild ? " scheduled" : "");
1139
+ var badge = document.createElement("span");
1140
+ badge.className = "mobile-loop-role-badge" + roleCls;
1141
+ badge.textContent = roleName + iterSuffix;
1142
+ textSpan.appendChild(badge);
1143
+ }
1144
+ el.appendChild(textSpan);
1145
+
1146
+ (function (id) {
1147
+ el.addEventListener("click", function () {
1148
+ if (ctx.ws && ctx.connected) {
1149
+ ctx.ws.send(JSON.stringify({ type: "switch_session", id: id }));
1150
+ }
1151
+ dismissOverlayPanels();
1152
+ closeMobileSheet();
1153
+ });
1154
+ })(s.id);
1155
+
1156
+ return el;
1157
+ }
1158
+
1159
+ // Helper: create a mobile loop run sub-group (collapsible time group)
1160
+ function createMobileLoopRun(parentGk, startedAtKey, sessions, isRalph) {
1161
+ var runGk = parentGk + ":" + startedAtKey;
1162
+ var expanded = expandedMobileLoopRuns.has(runGk);
1163
+ var startedAt = Number(startedAtKey);
1164
+ var timeLabel = startedAt ? new Date(startedAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "Unknown";
1165
+
1166
+ var hasActive = false;
1167
+ var anyProcessing = false;
1168
+ var latestSession = sessions[0];
1169
+ for (var i = 0; i < sessions.length; i++) {
1170
+ if (sessions[i].active) hasActive = true;
1171
+ if (sessions[i].isProcessing) anyProcessing = true;
1172
+ if ((sessions[i].lastActivity || 0) > (latestSession.lastActivity || 0)) {
1173
+ latestSession = sessions[i];
1174
+ }
1175
+ }
1176
+
1177
+ var wrapper = document.createElement("div");
1178
+ wrapper.className = "mobile-loop-run-wrapper";
1179
+
1180
+ var header = document.createElement("button");
1181
+ header.className = "mobile-loop-run" + (hasActive ? " active" : "") + (expanded ? " expanded" : "") + (isRalph ? "" : " scheduled");
1182
+
1183
+ var chevron = document.createElement("span");
1184
+ chevron.className = "mobile-loop-chevron";
1185
+ chevron.innerHTML = iconHtml("chevron-right");
1186
+ header.appendChild(chevron);
1187
+
1188
+ var label = document.createElement("span");
1189
+ label.className = "mobile-loop-run-time";
1190
+ var labelHtml = "";
1191
+ if (anyProcessing) {
1192
+ labelHtml += '<span class="mobile-session-processing"></span> ';
1193
+ }
1194
+ labelHtml += escapeHtml(timeLabel);
1195
+ label.innerHTML = labelHtml;
1196
+ header.appendChild(label);
1197
+
1198
+ var countBadge = document.createElement("span");
1199
+ countBadge.className = "mobile-loop-count" + (isRalph ? "" : " scheduled");
1200
+ countBadge.textContent = String(sessions.length);
1201
+ header.appendChild(countBadge);
1202
+
1203
+ header.addEventListener("click", (function (rk) {
1204
+ return function (e) {
1205
+ e.stopPropagation();
1206
+ if (expandedMobileLoopRuns.has(rk)) {
1207
+ expandedMobileLoopRuns.delete(rk);
1208
+ } else {
1209
+ expandedMobileLoopRuns.add(rk);
1210
+ }
1211
+ refreshMobileChatSheet();
1212
+ };
1213
+ })(runGk));
1214
+
1215
+ wrapper.appendChild(header);
1216
+
1217
+ if (expanded) {
1218
+ var childContainer = document.createElement("div");
1219
+ childContainer.className = "mobile-loop-children";
1220
+ for (var k = 0; k < sessions.length; k++) {
1221
+ childContainer.appendChild(createMobileLoopChild(sessions[k]));
1222
+ }
1223
+ wrapper.appendChild(childContainer);
1224
+ }
1225
+
1226
+ return wrapper;
1227
+ }
1228
+
1229
+ // Helper: create a mobile loop group element (collapsible group header)
1230
+ function createMobileLoopGroup(loopId, children, groupKey) {
1231
+ var gk = groupKey || loopId;
1232
+
1233
+ // Sub-group children by startedAt (each run)
1234
+ var runMap = {};
1235
+ for (var i = 0; i < children.length; i++) {
1236
+ var runKey = String(children[i].loop && children[i].loop.startedAt || 0);
1237
+ if (!runMap[runKey]) runMap[runKey] = [];
1238
+ runMap[runKey].push(children[i]);
1239
+ }
1240
+ var runKeys = Object.keys(runMap);
1241
+
1242
+ // Sort each run's children by iteration then role
1243
+ for (var ri = 0; ri < runKeys.length; ri++) {
1244
+ runMap[runKeys[ri]].sort(function (a, b) {
1245
+ var ai = (a.loop && a.loop.iteration) || 0;
1246
+ var bi = (b.loop && b.loop.iteration) || 0;
1247
+ if (ai !== bi) return ai - bi;
1248
+ var ar = (a.loop && a.loop.role === "judge") ? 1 : 0;
1249
+ var br = (b.loop && b.loop.role === "judge") ? 1 : 0;
1250
+ return ar - br;
1251
+ });
1252
+ }
1253
+
1254
+ // Sort runs by startedAt descending (newest first)
1255
+ runKeys.sort(function (a, b) { return Number(b) - Number(a); });
1256
+
1257
+ var expanded = expandedMobileLoopGroups.has(gk);
1258
+ var hasActive = false;
1259
+ var anyProcessing = false;
1260
+ var latestSession = children[0];
1261
+ for (var ci = 0; ci < children.length; ci++) {
1262
+ if (children[ci].active) hasActive = true;
1263
+ if (children[ci].isProcessing) anyProcessing = true;
1264
+ if ((children[ci].lastActivity || 0) > (latestSession.lastActivity || 0)) {
1265
+ latestSession = children[ci];
1266
+ }
1267
+ }
1268
+
1269
+ var loopName = (children[0].loop && children[0].loop.name) || "Ralph Loop";
1270
+ var isRalph = children[0].loop && children[0].loop.source === "ralph";
1271
+ var isCrafting = false;
1272
+ for (var j = 0; j < children.length; j++) {
1273
+ if (children[j].loop && children[j].loop.role === "crafting") isCrafting = true;
1274
+ }
1275
+ var runCount = runKeys.length;
1276
+
1277
+ var wrapper = document.createElement("div");
1278
+ wrapper.className = "mobile-loop-wrapper";
1279
+
1280
+ // Group header row
1281
+ var header = document.createElement("button");
1282
+ header.className = "mobile-loop-group" + (hasActive ? " active" : "") + (expanded ? " expanded" : "") + (isRalph ? "" : " scheduled");
1283
+
1284
+ var chevron = document.createElement("span");
1285
+ chevron.className = "mobile-loop-chevron";
1286
+ chevron.innerHTML = iconHtml("chevron-right");
1287
+ header.appendChild(chevron);
1288
+
1289
+ var iconSpan = document.createElement("span");
1290
+ var groupIcon = isRalph ? "repeat" : "calendar-clock";
1291
+ iconSpan.className = "mobile-loop-icon" + (isRalph ? "" : " scheduled");
1292
+ iconSpan.innerHTML = iconHtml(groupIcon);
1293
+ header.appendChild(iconSpan);
1294
+
1295
+ if (anyProcessing) {
1296
+ var dot = document.createElement("span");
1297
+ dot.className = "mobile-session-processing";
1298
+ header.appendChild(dot);
1299
+ }
1300
+
1301
+ var nameSpan = document.createElement("span");
1302
+ nameSpan.className = "mobile-loop-name";
1303
+ nameSpan.textContent = loopName;
1304
+ header.appendChild(nameSpan);
1305
+
1306
+ if (isCrafting && children.length === 1) {
1307
+ var craftBadge = document.createElement("span");
1308
+ craftBadge.className = "mobile-loop-badge crafting";
1309
+ craftBadge.textContent = "Crafting";
1310
+ header.appendChild(craftBadge);
1311
+ } else {
1312
+ var countBadge = document.createElement("span");
1313
+ countBadge.className = "mobile-loop-count" + (isRalph ? "" : " scheduled");
1314
+ var countLabel = runCount === 1 ? String(children.length) : runCount + (runCount === 1 ? " run" : " runs");
1315
+ countBadge.textContent = countLabel;
1316
+ header.appendChild(countBadge);
1317
+ }
1318
+
1319
+ // Chevron toggles expansion
1320
+ header.addEventListener("click", (function (lid) {
1321
+ return function (e) {
1322
+ e.stopPropagation();
1323
+ if (expandedMobileLoopGroups.has(lid)) {
1324
+ expandedMobileLoopGroups.delete(lid);
1325
+ } else {
1326
+ expandedMobileLoopGroups.add(lid);
1327
+ }
1328
+ refreshMobileChatSheet();
1329
+ };
1330
+ })(gk));
1331
+
1332
+ wrapper.appendChild(header);
1333
+
1334
+ // Expanded: show runs
1335
+ if (expanded) {
1336
+ var childContainer = document.createElement("div");
1337
+ childContainer.className = "mobile-loop-children";
1338
+
1339
+ if (runCount === 1) {
1340
+ var singleRun = runMap[runKeys[0]];
1341
+ for (var sk = 0; sk < singleRun.length; sk++) {
1342
+ childContainer.appendChild(createMobileLoopChild(singleRun[sk]));
1343
+ }
1344
+ } else {
1345
+ for (var rk = 0; rk < runKeys.length; rk++) {
1346
+ childContainer.appendChild(createMobileLoopRun(gk, runKeys[rk], runMap[runKeys[rk]], isRalph));
1347
+ }
1348
+ }
1349
+
1350
+ wrapper.appendChild(childContainer);
1351
+ }
1352
+
1353
+ return wrapper;
1354
+ }
1355
+
1356
+ // Helper: render sorted sessions into a container with date groups (with loop session grouping)
1108
1357
  function renderMobileSessionsInto(container) {
1109
1358
  var newBtn = document.createElement("button");
1110
1359
  newBtn.className = "mobile-session-new";
@@ -1117,14 +1366,51 @@ function renderMobileSessionsInto(container) {
1117
1366
  });
1118
1367
  container.appendChild(newBtn);
1119
1368
 
1120
- var sorted = cachedSessions.slice().sort(function (a, b) {
1369
+ // Partition: loop sessions vs normal sessions (same logic as desktop renderSessionList)
1370
+ var loopGroups = {};
1371
+ var normalSessions = [];
1372
+ for (var i = 0; i < cachedSessions.length; i++) {
1373
+ var s = cachedSessions[i];
1374
+ if (s.loop && s.loop.loopId && s.loop.role === "crafting" && s.loop.source !== "ralph") {
1375
+ continue;
1376
+ } else if (s.loop && s.loop.loopId) {
1377
+ var startedAt = s.loop.startedAt || 0;
1378
+ var dateStr = startedAt ? new Date(startedAt).toISOString().slice(0, 10) : "unknown";
1379
+ var groupKey = s.loop.loopId + ":" + dateStr;
1380
+ if (!loopGroups[groupKey]) loopGroups[groupKey] = [];
1381
+ loopGroups[groupKey].push(s);
1382
+ } else {
1383
+ normalSessions.push(s);
1384
+ }
1385
+ }
1386
+
1387
+ // Build virtual items
1388
+ var items = [];
1389
+ for (var j = 0; j < normalSessions.length; j++) {
1390
+ items.push({ type: "session", data: normalSessions[j], lastActivity: normalSessions[j].lastActivity || 0 });
1391
+ }
1392
+ var groupKeys = Object.keys(loopGroups);
1393
+ for (var k = 0; k < groupKeys.length; k++) {
1394
+ var gk = groupKeys[k];
1395
+ var children = loopGroups[gk];
1396
+ var realLoopId = children[0].loop.loopId;
1397
+ var maxActivity = 0;
1398
+ for (var m = 0; m < children.length; m++) {
1399
+ var act = children[m].lastActivity || 0;
1400
+ if (act > maxActivity) maxActivity = act;
1401
+ }
1402
+ items.push({ type: "loop", loopId: realLoopId, groupKey: gk, children: children, lastActivity: maxActivity });
1403
+ }
1404
+
1405
+ // Sort by lastActivity descending
1406
+ items.sort(function (a, b) {
1121
1407
  return (b.lastActivity || 0) - (a.lastActivity || 0);
1122
1408
  });
1123
1409
 
1124
1410
  var currentGroup = "";
1125
- for (var si = 0; si < sorted.length; si++) {
1126
- var s = sorted[si];
1127
- var group = getDateGroup(s.lastActivity || 0);
1411
+ for (var n = 0; n < items.length; n++) {
1412
+ var item = items[n];
1413
+ var group = getDateGroup(item.lastActivity || 0);
1128
1414
  if (group !== currentGroup) {
1129
1415
  currentGroup = group;
1130
1416
  var header = document.createElement("div");
@@ -1132,7 +1418,11 @@ function renderMobileSessionsInto(container) {
1132
1418
  header.textContent = group;
1133
1419
  container.appendChild(header);
1134
1420
  }
1135
- container.appendChild(createMobileSessionItem(s));
1421
+ if (item.type === "loop") {
1422
+ container.appendChild(createMobileLoopGroup(item.loopId, item.children, item.groupKey));
1423
+ } else {
1424
+ container.appendChild(createMobileSessionItem(item.data));
1425
+ }
1136
1426
  }
1137
1427
  }
1138
1428
 
@@ -2141,15 +2431,13 @@ function showMateCtxMenu(anchorEl, mate) {
2141
2431
  menu.appendChild(editItem);
2142
2432
 
2143
2433
  var removeItem = document.createElement("button");
2144
- removeItem.className = "project-ctx-item project-ctx-delete";
2145
- removeItem.innerHTML = iconHtml("trash-2") + " <span>Remove Mate</span>";
2434
+ removeItem.className = "project-ctx-item";
2435
+ removeItem.innerHTML = iconHtml("star-off") + " <span>Remove from favorites</span>";
2146
2436
  removeItem.addEventListener("click", function (e) {
2147
2437
  e.stopPropagation();
2148
- var iconRect = anchorEl.getBoundingClientRect();
2149
- spawnDustParticles(iconRect.left + iconRect.width / 2, iconRect.top + iconRect.height / 2);
2150
2438
  closeUserCtxMenu();
2151
2439
  if (ctx.sendWs) {
2152
- ctx.sendWs({ type: "mate_delete", mateId: mate.id });
2440
+ ctx.sendWs({ type: "dm_remove_favorite", targetUserId: mate.id });
2153
2441
  }
2154
2442
  });
2155
2443
  menu.appendChild(removeItem);
@@ -3522,8 +3810,20 @@ export function renderUserStrip(allUsers, onlineUserIds, myUserId, dmFavorites,
3522
3810
  }
3523
3811
  }
3524
3812
 
3525
- // Render mates
3526
- for (var mi = 0; mi < cachedMates.length; mi++) {
3813
+ // Render mates (only favorites, built-in first, then user-created)
3814
+ var favoriteMates = cachedMates.filter(function (m) {
3815
+ if (cachedDmRemovedUsers[m.id]) return false;
3816
+ if (cachedDmFavorites.indexOf(m.id) !== -1) return true;
3817
+ if (cachedDmUnread[m.id] && cachedDmUnread[m.id] > 0) return true;
3818
+ return false;
3819
+ });
3820
+ var sortedMates = favoriteMates.sort(function (a, b) {
3821
+ var aBuiltin = a.builtinKey ? 1 : 0;
3822
+ var bBuiltin = b.builtinKey ? 1 : 0;
3823
+ if (aBuiltin !== bBuiltin) return bBuiltin - aBuiltin;
3824
+ return (a.createdAt || 0) - (b.createdAt || 0);
3825
+ });
3826
+ for (var mi = 0; mi < sortedMates.length; mi++) {
3527
3827
  (function (mate) {
3528
3828
  var mp = mate.profile || {};
3529
3829
  var mateSlug = "mate-" + mate.id;
@@ -3598,7 +3898,7 @@ export function renderUserStrip(allUsers, onlineUserIds, myUserId, dmFavorites,
3598
3898
  });
3599
3899
 
3600
3900
  container.appendChild(el);
3601
- })(cachedMates[mi]);
3901
+ })(sortedMates[mi]);
3602
3902
  }
3603
3903
 
3604
3904
  // Show container if we have mates even with no other users
@@ -3635,13 +3935,12 @@ function toggleDmUserPicker(anchorEl) {
3635
3935
  var searchInput = document.createElement("input");
3636
3936
  searchInput.className = "dm-user-picker-search";
3637
3937
  searchInput.type = "text";
3638
- searchInput.placeholder = "Search users...";
3938
+ searchInput.placeholder = "Search mates and users...";
3639
3939
  picker.appendChild(searchInput);
3640
3940
 
3641
- // Scrollable list
3941
+ // User list element (appended later, after USERS label)
3642
3942
  var listEl = document.createElement("div");
3643
3943
  listEl.className = "dm-user-picker-list";
3644
- picker.appendChild(listEl);
3645
3944
 
3646
3945
  // Position the picker above the + button
3647
3946
  document.body.appendChild(picker);
@@ -3698,10 +3997,193 @@ function toggleDmUserPicker(anchorEl) {
3698
3997
  }
3699
3998
  }
3700
3999
 
4000
+ // --- MATES section ---
4001
+ var matesSectionLabel = document.createElement("div");
4002
+ matesSectionLabel.className = "dm-user-picker-section";
4003
+ matesSectionLabel.textContent = "Mates";
4004
+ picker.appendChild(matesSectionLabel);
4005
+
4006
+ var matesListEl = document.createElement("div");
4007
+ matesListEl.className = "dm-user-picker-list dm-mates-list";
4008
+ picker.appendChild(matesListEl);
4009
+
4010
+ // Update scroll gradient hint
4011
+ function updateMatesScrollHint() {
4012
+ var isOverflow = matesListEl.scrollHeight > matesListEl.clientHeight + 2;
4013
+ if (!isOverflow) {
4014
+ matesListEl.classList.add("no-overflow");
4015
+ matesListEl.classList.remove("scrolled-bottom");
4016
+ return;
4017
+ }
4018
+ matesListEl.classList.remove("no-overflow");
4019
+ var atBottom = matesListEl.scrollTop + matesListEl.clientHeight >= matesListEl.scrollHeight - 4;
4020
+ if (atBottom) {
4021
+ matesListEl.classList.add("scrolled-bottom");
4022
+ } else {
4023
+ matesListEl.classList.remove("scrolled-bottom");
4024
+ }
4025
+ }
4026
+ matesListEl.addEventListener("scroll", updateMatesScrollHint);
4027
+
4028
+ function renderMatesList(filter) {
4029
+ matesListEl.innerHTML = "";
4030
+ var allMates = cachedMates || [];
4031
+ if (filter) {
4032
+ var lf = filter.toLowerCase();
4033
+ allMates = allMates.filter(function (m) {
4034
+ var name = (m.profile && m.profile.displayName) || m.name || "";
4035
+ return name.toLowerCase().indexOf(lf) !== -1;
4036
+ });
4037
+ }
4038
+ // Build unified list: installed builtins, deleted builtins, user-created
4039
+ var availBuiltins = (ctx.availableBuiltins && ctx.availableBuiltins()) || [];
4040
+ var entries = [];
4041
+ // 1. Installed builtin mates
4042
+ for (var si = 0; si < allMates.length; si++) {
4043
+ if (allMates[si].builtinKey) entries.push({ type: "mate", data: allMates[si] });
4044
+ }
4045
+ // 2. Deleted builtins (only when not filtering)
4046
+ if (!filter) {
4047
+ for (var di = 0; di < availBuiltins.length; di++) {
4048
+ entries.push({ type: "deleted", data: availBuiltins[di] });
4049
+ }
4050
+ }
4051
+ // 3. User-created mates
4052
+ var userMates = allMates.filter(function (m) { return !m.builtinKey; });
4053
+ userMates.sort(function (a, b) { return (a.createdAt || 0) - (b.createdAt || 0); });
4054
+ for (var ui = 0; ui < userMates.length; ui++) {
4055
+ entries.push({ type: "mate", data: userMates[ui] });
4056
+ }
4057
+
4058
+ for (var i = 0; i < entries.length; i++) {
4059
+ var entry = entries[i];
4060
+ if (entry.type === "deleted") {
4061
+ // Deleted builtin: show with "+ Add" button
4062
+ (function (b) {
4063
+ var bItem = document.createElement("div");
4064
+ bItem.className = "dm-user-picker-item dm-user-picker-builtin-item";
4065
+ bItem.style.opacity = "0.5";
4066
+ var bAv = document.createElement("img");
4067
+ bAv.className = "dm-user-picker-avatar";
4068
+ bAv.src = b.avatarCustom || "";
4069
+ bAv.alt = b.displayName;
4070
+ bItem.appendChild(bAv);
4071
+ var bNameWrap = document.createElement("div");
4072
+ bNameWrap.style.cssText = "flex:1;min-width:0;";
4073
+ var bName = document.createElement("span");
4074
+ bName.className = "dm-user-picker-name";
4075
+ bName.textContent = b.displayName;
4076
+ bNameWrap.appendChild(bName);
4077
+ var bBio = document.createElement("div");
4078
+ bBio.style.cssText = "font-size:11px;color:var(--text-dimmer);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;";
4079
+ bBio.textContent = "Deleted";
4080
+ bNameWrap.appendChild(bBio);
4081
+ bItem.appendChild(bNameWrap);
4082
+ var bAddBtn = document.createElement("button");
4083
+ bAddBtn.style.cssText = "border:none;background:none;cursor:pointer;padding:2px 6px;color:var(--accent, #6366f1);font-size:12px;font-weight:600;white-space:nowrap;";
4084
+ bAddBtn.textContent = "+ Add";
4085
+ bAddBtn.title = "Re-add " + b.displayName;
4086
+ bAddBtn.addEventListener("click", function (e) {
4087
+ e.stopPropagation();
4088
+ if (ctx.sendWs) ctx.sendWs({ type: "mate_readd_builtin", builtinKey: b.key });
4089
+ closeDmUserPicker();
4090
+ });
4091
+ bItem.appendChild(bAddBtn);
4092
+ bItem.addEventListener("click", function () {
4093
+ if (ctx.sendWs) ctx.sendWs({ type: "mate_readd_builtin", builtinKey: b.key });
4094
+ closeDmUserPicker();
4095
+ });
4096
+ matesListEl.appendChild(bItem);
4097
+ })(entry.data);
4098
+ } else {
4099
+ // Normal mate
4100
+ (function (m) {
4101
+ var mp = m.profile || {};
4102
+ var isFav = cachedDmFavorites.indexOf(m.id) !== -1;
4103
+ var item = document.createElement("div");
4104
+ item.className = "dm-user-picker-item";
4105
+ if (isFav) item.classList.add("dm-picker-fav");
4106
+ var av = document.createElement("img");
4107
+ av.className = "dm-user-picker-avatar";
4108
+ av.src = mateAvatarUrl(m, 28);
4109
+ av.alt = mp.displayName || m.name || "Mate";
4110
+ item.appendChild(av);
4111
+ var nameWrap = document.createElement("div");
4112
+ nameWrap.style.cssText = "flex:1;min-width:0;";
4113
+ var name = document.createElement("span");
4114
+ name.className = "dm-user-picker-name";
4115
+ name.textContent = mp.displayName || m.name || "Mate";
4116
+ nameWrap.appendChild(name);
4117
+ if (m.bio) {
4118
+ var bio = document.createElement("div");
4119
+ bio.style.cssText = "font-size:11px;color:var(--text-dimmer);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;";
4120
+ bio.textContent = m.bio;
4121
+ nameWrap.appendChild(bio);
4122
+ }
4123
+ item.appendChild(nameWrap);
4124
+ // Delete button with inline confirm
4125
+ var delBtn = document.createElement("button");
4126
+ delBtn.className = "dm-picker-del-btn";
4127
+ delBtn.innerHTML = iconHtml("trash-2");
4128
+ delBtn.title = "Delete mate";
4129
+ delBtn.addEventListener("click", function (e) {
4130
+ e.stopPropagation();
4131
+ var origHtml = item.innerHTML;
4132
+ item.innerHTML = "";
4133
+ item.style.justifyContent = "center";
4134
+ item.style.gap = "6px";
4135
+ var confirmMsg = document.createElement("span");
4136
+ confirmMsg.style.cssText = "font-size:12px;color:var(--text-dimmer);";
4137
+ confirmMsg.textContent = m.builtinKey ? "Delete? You can re-add later." : "Delete permanently?";
4138
+ item.appendChild(confirmMsg);
4139
+ var yesBtn = document.createElement("button");
4140
+ yesBtn.style.cssText = "border:none;background:var(--danger,#e74c3c);color:#fff;padding:3px 10px;border-radius:4px;font-size:12px;cursor:pointer;";
4141
+ yesBtn.textContent = "Delete";
4142
+ yesBtn.addEventListener("click", function (e2) {
4143
+ e2.stopPropagation();
4144
+ if (ctx.sendWs) ctx.sendWs({ type: "mate_delete", mateId: m.id });
4145
+ closeDmUserPicker();
4146
+ });
4147
+ item.appendChild(yesBtn);
4148
+ var noBtn = document.createElement("button");
4149
+ noBtn.style.cssText = "border:1px solid var(--border);background:none;color:var(--text);padding:3px 10px;border-radius:4px;font-size:12px;cursor:pointer;";
4150
+ noBtn.textContent = "Cancel";
4151
+ noBtn.addEventListener("click", function (e2) {
4152
+ e2.stopPropagation();
4153
+ item.innerHTML = origHtml;
4154
+ item.style.justifyContent = "";
4155
+ item.style.gap = "";
4156
+ refreshIcons();
4157
+ });
4158
+ item.appendChild(noBtn);
4159
+ });
4160
+ item.appendChild(delBtn);
4161
+ item.addEventListener("click", function () {
4162
+ if (ctx.openDm) ctx.openDm(m.id);
4163
+ if (!isFav && ctx.sendWs) ctx.sendWs({ type: "dm_add_favorite", targetUserId: m.id });
4164
+ closeDmUserPicker();
4165
+ });
4166
+ matesListEl.appendChild(item);
4167
+ })(entry.data);
4168
+ }
4169
+ }
4170
+
4171
+ if (entries.length === 0 && filter) {
4172
+ var emptyEl = document.createElement("div");
4173
+ emptyEl.className = "dm-user-picker-empty";
4174
+ emptyEl.textContent = "No mates found";
4175
+ matesListEl.appendChild(emptyEl);
4176
+ }
4177
+ refreshIcons();
4178
+ requestAnimationFrame(updateMatesScrollHint);
4179
+ }
4180
+
3701
4181
  // Create Mate option
3702
4182
  var createMateEl = document.createElement("div");
3703
4183
  createMateEl.className = "dm-user-picker-create-mate";
3704
- createMateEl.innerHTML = iconHtml("bot") + " <span>Create a Mate</span>";
4184
+ var hasCustomMates = (cachedMates || []).some(function (m) { return !m.builtinKey; });
4185
+ var createMateLabel = hasCustomMates ? "Create a Mate" : "Create a Mate for what you're doing";
4186
+ createMateEl.innerHTML = iconHtml("bot") + " <span>" + createMateLabel + "</span>";
3705
4187
  createMateEl.addEventListener("click", function () {
3706
4188
  closeDmUserPicker();
3707
4189
  if (ctx.openMateWizard) ctx.openMateWizard();
@@ -3719,10 +4201,14 @@ function toggleDmUserPicker(anchorEl) {
3719
4201
  sectionLabel.className = "dm-user-picker-section";
3720
4202
  sectionLabel.textContent = "Users";
3721
4203
  picker.appendChild(sectionLabel);
4204
+ picker.appendChild(listEl);
3722
4205
 
4206
+ renderMatesList("");
3723
4207
  renderPickerList("");
3724
4208
  searchInput.addEventListener("input", function () {
3725
- renderPickerList(searchInput.value);
4209
+ var val = searchInput.value;
4210
+ renderMatesList(val);
4211
+ renderPickerList(val);
3726
4212
  });
3727
4213
 
3728
4214
  // Focus search
@@ -495,13 +495,16 @@ export function toggleDarkMode() {
495
495
  updateToggleIcon();
496
496
  }
497
497
 
498
- // Update the toggle switch checkbox state
498
+ // Update all theme toggle checkboxes
499
499
  function updateToggleIcon() {
500
- var checkbox = document.getElementById("theme-toggle-check");
501
- if (!checkbox) return;
502
500
  var mode = getEffectiveMode();
503
- // checked = light mode (thumb slides right, covering moon)
504
- checkbox.checked = (mode === "light");
501
+ var isLight = mode === "light";
502
+ // Legacy top-bar toggle (may not exist)
503
+ var checkbox = document.getElementById("theme-toggle-check");
504
+ if (checkbox) checkbox.checked = isLight;
505
+ // User settings toggle
506
+ var usToggle = document.getElementById("us-theme-toggle");
507
+ if (usToggle) usToggle.checked = isLight;
505
508
  }
506
509
 
507
510
  // --- Theme picker UI ---
@@ -671,13 +674,8 @@ export function initTheme() {
671
674
  // Load all themes from server, then apply properly
672
675
  loadThemes();
673
676
 
674
- // Wire up title bar toggle switch
675
- var toggleCheck = document.getElementById("theme-toggle-check");
676
- if (toggleCheck) {
677
- toggleCheck.addEventListener("change", function () {
678
- toggleDarkMode();
679
- });
680
- }
677
+ // Theme toggle is now in User Settings (us-theme-toggle)
678
+ // Wired up in user-settings.js initUserSettings()
681
679
 
682
680
  // Listen for system preference changes (only applies if user has no manual override)
683
681
  if (window.matchMedia) {