clay-server 3.7.0 → 3.8.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 (40) hide show
  1. package/lib/builtin-mates.js +14 -14
  2. package/lib/mates.js +2 -6
  3. package/lib/os-users.js +17 -1
  4. package/lib/project-http.js +34 -1
  5. package/lib/project-image.js +87 -1
  6. package/lib/project-mate-interaction.js +7 -7
  7. package/lib/project-sessions.js +1 -1
  8. package/lib/project-user-mention.js +4 -4
  9. package/lib/project.js +6 -4
  10. package/lib/public/css/avatar-imprints.css +46 -0
  11. package/lib/public/css/generated-images.css +242 -0
  12. package/lib/public/css/icon-strip.css +3 -3
  13. package/lib/public/css/profile.css +123 -95
  14. package/lib/public/css/session-actions.css +40 -9
  15. package/lib/public/css/user-settings.css +1 -1
  16. package/lib/public/index.html +3 -3
  17. package/lib/public/modules/app-cursors.js +2 -2
  18. package/lib/public/modules/app-dm.js +1 -8
  19. package/lib/public/modules/app-messages.js +2 -2
  20. package/lib/public/modules/app-projects.js +7 -6
  21. package/lib/public/modules/app-rate-limit.js +0 -1
  22. package/lib/public/modules/app-rendering.js +0 -3
  23. package/lib/public/modules/avatar-imprint.js +134 -0
  24. package/lib/public/modules/avatar.js +25 -19
  25. package/lib/public/modules/debate.js +10 -5
  26. package/lib/public/modules/generated-images.js +211 -2
  27. package/lib/public/modules/mate-sidebar.js +2 -2
  28. package/lib/public/modules/mention.js +26 -16
  29. package/lib/public/modules/profile.js +73 -190
  30. package/lib/public/modules/session-actions.js +5 -3
  31. package/lib/public/modules/sidebar-mates.js +5 -4
  32. package/lib/public/modules/sidebar-sessions.js +1 -1
  33. package/lib/public/style.css +2 -1
  34. package/lib/server-auth.js +2 -2
  35. package/lib/server-dm.js +8 -8
  36. package/lib/server-mates.js +3 -3
  37. package/lib/server-settings.js +2 -2
  38. package/lib/server.js +5 -4
  39. package/lib/users.js +4 -4
  40. package/package.json +1 -1
@@ -112,7 +112,7 @@ function buildMentionCandidates() {
112
112
  kind: "mate",
113
113
  id: m.id,
114
114
  name: (m.profile && m.profile.displayName) || m.name || "Mate",
115
- color: (m.profile && m.profile.avatarColor) || "#6c5ce7",
115
+ color: (m.profile && m.profile.avatarColor) || "#5857fc",
116
116
  avatarSrc: mateAvatarUrl(m, 24),
117
117
  vendor: m.vendor || "claude",
118
118
  bio: m.bio || (m.profile && m.profile.bio) || "",
@@ -134,9 +134,8 @@ function buildMentionCandidates() {
134
134
  kind: "user",
135
135
  id: u.id,
136
136
  name: u.displayName || u.username || "User",
137
- color: u.avatarColor || "#7c3aed",
138
- // Build dicebear url from style+seed; matches userAvatarUrl shape.
139
- avatarSrc: "https://api.dicebear.com/7.x/" + (u.avatarStyle || "thumbs") + "/svg?seed=" + encodeURIComponent(u.avatarSeed || u.username || u.id),
137
+ color: u.avatarColor || "#5857fc",
138
+ avatarSrc: userAvatarUrl(u, 24),
140
139
  vendor: null,
141
140
  bio: u.username ? "@" + u.username : "",
142
141
  primary: false,
@@ -424,7 +423,7 @@ export function stickyReapplyMention() {
424
423
  var kind = selectedMateKind;
425
424
  var id = selectedMateId;
426
425
  var name = selectedMateName;
427
- var color = selectedMateColor || "#6c5ce7";
426
+ var color = selectedMateColor || "#5857fc";
428
427
  var avatarSrc = selectedMateAvatar || "";
429
428
  // Reset index but keep selection
430
429
  mentionAtIdx = -1;
@@ -516,7 +515,7 @@ export function handleMentionStart(msg) {
516
515
  header.className = "dm-bubble-header";
517
516
  var nameSpan = document.createElement("span");
518
517
  nameSpan.className = "dm-bubble-name";
519
- nameSpan.style.color = msg.avatarColor || "#6c5ce7";
518
+ nameSpan.style.color = msg.avatarColor || "#5857fc";
520
519
  nameSpan.textContent = msg.mateName || "Mate";
521
520
  header.appendChild(nameSpan);
522
521
 
@@ -556,7 +555,7 @@ export function handleMentionStart(msg) {
556
555
  // Project chat: mention block style
557
556
  currentMentionEl = document.createElement("div");
558
557
  currentMentionEl.className = "msg-mention";
559
- currentMentionEl.style.setProperty("--mention-color", msg.avatarColor || "#6c5ce7");
558
+ currentMentionEl.style.setProperty("--mention-color", msg.avatarColor || "#5857fc");
560
559
 
561
560
  var header = document.createElement("div");
562
561
  header.className = "mention-header";
@@ -754,15 +753,13 @@ function isDmLayout() {
754
753
 
755
754
  function getMyAvatarSrc() {
756
755
  if (document.body.dataset.myAvatarUrl) return document.body.dataset.myAvatarUrl;
757
- var myUser = null;
758
- try { myUser = JSON.parse(localStorage.getItem("clay_my_user") || "null"); } catch (e) {}
756
+ var myUser = (store.get('cachedAllUsers') || []).find(function (u) { return u.id === store.get('myUserId'); });
759
757
  return userAvatarUrl(myUser || {}, 36);
760
758
  }
761
759
 
762
760
  function getMyDisplayName() {
763
761
  if (document.body.dataset.myDisplayName) return document.body.dataset.myDisplayName;
764
- var myUser = null;
765
- try { myUser = JSON.parse(localStorage.getItem("clay_my_user") || "null"); } catch (e) {}
762
+ var myUser = (store.get('cachedAllUsers') || []).find(function (u) { return u.id === store.get('myUserId'); });
766
763
  return (myUser && (myUser.displayName || myUser.username)) || "Me";
767
764
  }
768
765
 
@@ -772,14 +769,27 @@ function timeStr() {
772
769
  }
773
770
 
774
771
  function buildMentionAvatarUrl(meta) {
775
- return "https://api.dicebear.com/7.x/" + (meta.avatarStyle || "bottts") + "/svg?seed=" + encodeURIComponent(meta.avatarSeed || meta.mateId);
772
+ return mateAvatarUrl({
773
+ id: meta.mateId,
774
+ profile: {
775
+ avatarStyle: meta.avatarStyle || "imprint",
776
+ avatarSeed: meta.avatarSeed || meta.mateId,
777
+ avatarColor: meta.avatarColor || "#5857fc",
778
+ avatarCustom: meta.avatarCustom || "",
779
+ },
780
+ }, 36);
776
781
  }
777
782
 
778
783
  function buildUserAvatarUrlFromEntry(entry, who) {
779
784
  // who = "from" | "target"
780
- var style = entry[who + "AvatarStyle"] || "thumbs";
781
785
  var seed = entry[who + "AvatarSeed"] || entry[who === "from" ? "fromName" : "targetName"] || entry[who === "from" ? "from" : "targetUserId"];
782
- return "https://api.dicebear.com/7.x/" + style + "/svg?seed=" + encodeURIComponent(seed || "user");
786
+ return userAvatarUrl({
787
+ id: entry[who === "from" ? "from" : "targetUserId"],
788
+ avatarStyle: entry[who + "AvatarStyle"] || "imprint",
789
+ avatarSeed: seed || "user",
790
+ avatarColor: entry[who + "AvatarColor"] || "#5857fc",
791
+ avatarCustom: entry[who + "AvatarCustom"] || "",
792
+ }, 36);
783
793
  }
784
794
 
785
795
  // Render a user-to-user @mention bubble (live receive AND history replay).
@@ -1021,7 +1031,7 @@ export function renderMentionResponse(entry) {
1021
1031
  header.className = "dm-bubble-header";
1022
1032
  var nameSpan = document.createElement("span");
1023
1033
  nameSpan.className = "dm-bubble-name";
1024
- nameSpan.style.color = entry.avatarColor || "#6c5ce7";
1034
+ nameSpan.style.color = entry.avatarColor || "#5857fc";
1025
1035
  nameSpan.textContent = entry.mateName || "Mate";
1026
1036
  header.appendChild(nameSpan);
1027
1037
 
@@ -1050,7 +1060,7 @@ export function renderMentionResponse(entry) {
1050
1060
  // Project chat: use mention block style
1051
1061
  var el = document.createElement("div");
1052
1062
  el.className = "msg-mention";
1053
- el.style.setProperty("--mention-color", entry.avatarColor || "#6c5ce7");
1063
+ el.style.setProperty("--mention-color", entry.avatarColor || "#5857fc");
1054
1064
 
1055
1065
  var mheader = document.createElement("div");
1056
1066
  mheader.className = "mention-header";
@@ -1,31 +1,18 @@
1
- // User profile module — Discord-style popover for name, language, avatar
1
+ // User profile module — profile popovers for people and Mates
2
2
  // Stores profile server-side in ~/.clay/profile.json
3
- // Avatar generated via DiceBear API (deterministic SVG from seed)
3
+ // Generated identities use deterministic Clay Imprints.
4
4
 
5
5
  import { iconHtml, refreshIcons } from './icons.js';
6
6
  import { setSTTLang } from './stt.js';
7
- import { avatarUrl, mateAvatarUrl, AVATAR_STYLES } from './avatar.js';
7
+ import { avatarUrl, mateAvatarUrl, mateMarkUrl } from './avatar.js';
8
8
  import { store } from './store.js';
9
9
  import { applyTerminalFont } from './terminal-prefs.js';
10
10
 
11
11
  var ctx;
12
- var profile = { name: '', lang: 'en-US', avatarStyle: 'thumbs', avatarSeed: '', avatarColor: '#7c3aed', avatarCustom: '' };
12
+ var profile = { name: '', lang: 'en-US', avatarStyle: 'imprint', avatarSeed: '', avatarColor: '#5857fc', avatarCustom: '' };
13
13
  var profileUsername = '';
14
14
  var popoverEl = null;
15
15
  var saveTimer = null;
16
- var previewSeed = '';
17
-
18
- // AVATAR_STYLES imported from avatar.js
19
-
20
- var COLORS = [
21
- '#7c3aed', '#4f46e5', '#2563eb', '#0891b2',
22
- '#059669', '#65a30d', '#d97706', '#dc2626',
23
- '#db2777', '#6366f1', '#0d9488', '#ea580c',
24
- '#475569', '#1e293b', '#be123c', '#a21caf',
25
- '#0369a1', '#15803d',
26
- ];
27
-
28
- // avatarUrl imported from avatar.js
29
16
 
30
17
  function getAvatarSeed() {
31
18
  return profile.avatarSeed || 'anonymous';
@@ -68,7 +55,7 @@ export function applyToIsland() {
68
55
  var existingLetter = avatarWrap.querySelector('.user-island-avatar-letter');
69
56
  var url = profile.avatarCustom
70
57
  ? profile.avatarCustom
71
- : avatarUrl(profile.avatarStyle || 'thumbs', getAvatarSeed(), 32);
58
+ : avatarUrl(profile.avatarStyle || 'imprint', getAvatarSeed(), 32);
72
59
 
73
60
  if (existingImg) {
74
61
  existingImg.src = url;
@@ -81,6 +68,12 @@ export function applyToIsland() {
81
68
  }
82
69
 
83
70
  nameEl.textContent = displayName;
71
+ document.body.dataset.myAvatarUrl = url;
72
+ document.body.dataset.myDisplayName = displayName;
73
+ var messageAvatars = document.querySelectorAll('.dm-bubble-avatar-me, .icon-strip-me-avatar');
74
+ for (var avatarIndex = 0; avatarIndex < messageAvatars.length; avatarIndex++) {
75
+ messageAvatars[avatarIndex].src = url;
76
+ }
84
77
 
85
78
  // Show CTA if user hasn't personalized their name
86
79
  var ctaEl = document.querySelector('.user-island-cta');
@@ -104,7 +97,7 @@ export function showAvatarPositioner(img, objectUrl, onDone) {
104
97
  var offsetX = 0;
105
98
  var offsetY = 0;
106
99
 
107
- // Fit image so shorter side fills the circle
100
+ // Fit image so the shorter side fills the imprint frame.
108
101
  var baseScale = Math.max(viewSize / img.width, viewSize / img.height);
109
102
 
110
103
  function clampOffsets() {
@@ -287,22 +280,20 @@ function showPopover() {
287
280
  popoverEl.className = 'profile-popover';
288
281
 
289
282
  var displayName = profile.name || '';
290
- var currentColor = profile.avatarColor || '#7c3aed';
291
- var currentStyle = profile.avatarStyle || 'thumbs';
283
+ var currentStyle = profile.avatarStyle || 'imprint';
292
284
  var seed = getAvatarSeed();
293
- previewSeed = seed;
294
285
 
295
286
  var html = '';
296
287
 
297
288
  // Banner + close
298
- html += '<div class="profile-banner" style="background:' + currentColor + '">';
289
+ html += '<div class="profile-banner">';
299
290
  html += '<button class="profile-close-btn">&times;</button>';
300
291
  html += '</div>';
301
292
 
302
293
  // Avatar row (overlapping banner)
303
294
  html += '<div class="profile-avatar-row">';
304
295
  html += '<div class="profile-popover-avatar">';
305
- html += '<img class="profile-popover-avatar-img" src="' + (profile.avatarCustom || avatarUrl(currentStyle, seed, 80)) + '" alt="avatar">';
296
+ html += '<img class="profile-popover-avatar-img" src="' + (profile.avatarCustom || avatarUrl(currentStyle, seed, 80)) + '" alt="' + escapeAttr(displayName || 'Clay user') + '">';
306
297
  html += '</div>';
307
298
  html += '<div class="profile-name-display">' + escapeAttr(displayName || 'Awesome Clay User') + '</div>';
308
299
  html += '</div>';
@@ -316,40 +307,18 @@ function showPopover() {
316
307
  html += '<input type="text" class="profile-field-input" id="profile-name-input" value="' + escapeAttr(displayName) + '" placeholder="Enter your name..." maxlength="50" spellcheck="false" autocomplete="off">';
317
308
  html += '</div>';
318
309
 
319
- // Avatar picker
310
+ // Clay Imprint or custom photo
320
311
  html += '<div class="profile-field">';
321
- html += '<label class="profile-field-label">Avatar <button class="profile-shuffle-btn" title="Shuffle">' + iconHtml('shuffle') + '</button></label>';
322
- html += '<div class="profile-avatar-grid">';
323
- // Upload button as first cell
324
- var uploadActive = profile.avatarCustom ? ' profile-avatar-option-active' : '';
325
- html += '<button class="profile-avatar-option profile-avatar-upload' + uploadActive + '" title="Upload photo">';
326
- if (profile.avatarCustom) {
327
- html += '<img src="' + profile.avatarCustom + '" alt="Custom" class="profile-avatar-custom-preview">';
328
- } else {
329
- html += '<span class="profile-avatar-upload-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></span>';
330
- }
331
- html += '</button>';
332
- html += '<input type="file" id="profile-avatar-file" accept="image/*" style="display:none">';
333
- for (var j = 0; j < AVATAR_STYLES.length; j++) {
334
- var st = AVATAR_STYLES[j];
335
- var activeS = (!profile.avatarCustom && currentStyle === st.id) ? ' profile-avatar-option-active' : '';
336
- html += '<button class="profile-avatar-option' + activeS + '" data-style="' + st.id + '" title="' + st.name + '">';
337
- html += '<img src="' + avatarUrl(st.id, seed, 40) + '" alt="' + st.name + '">';
338
- html += '</button>';
339
- }
312
+ html += '<label class="profile-field-label">Identity</label>';
313
+ html += '<div class="profile-imprint-panel">';
314
+ html += '<div class="profile-imprint-preview"><img src="' + avatarUrl(currentStyle, seed, 72) + '" alt="Generated Clay Imprint"><span><strong>Clay Imprint</strong><small>Unique to this identity</small></span></div>';
315
+ html += '<div class="profile-imprint-actions">';
316
+ html += '<button class="profile-use-imprint' + (!profile.avatarCustom ? ' is-active' : '') + '" type="button">Use imprint</button>';
317
+ html += '<button class="profile-regenerate-imprint" type="button" title="Generate another imprint">' + iconHtml('shuffle') + ' New imprint</button>';
318
+ html += '<button class="profile-avatar-upload' + (profile.avatarCustom ? ' is-active' : '') + '" type="button">Upload photo</button>';
340
319
  html += '</div>';
341
320
  html += '</div>';
342
-
343
- // Color
344
- html += '<div class="profile-field">';
345
- html += '<label class="profile-field-label">Color</label>';
346
- html += '<div class="profile-color-grid">';
347
- for (var k = 0; k < COLORS.length; k++) {
348
- var c = COLORS[k];
349
- var activeC = (currentColor === c) ? ' profile-color-active' : '';
350
- html += '<button class="profile-color-swatch' + activeC + '" data-color="' + c + '" style="background:' + c + '"></button>';
351
- }
352
- html += '</div>';
321
+ html += '<input type="file" id="profile-avatar-file" accept="image/*" style="display:none">';
353
322
  html += '</div>';
354
323
 
355
324
  html += '</div>'; // close body
@@ -403,15 +372,11 @@ function showPopover() {
403
372
  profile.avatarCustom = data.avatar;
404
373
  applyToIsland();
405
374
  updatePopoverHeader();
406
- if (popoverEl) {
407
- popoverEl.querySelectorAll('.profile-avatar-option').forEach(function(b) {
408
- b.classList.remove('profile-avatar-option-active');
409
- });
410
- }
411
375
  if (uploadBtn) {
412
- uploadBtn.classList.add('profile-avatar-option-active');
413
- uploadBtn.innerHTML = '<img src="' + data.avatar + '" alt="Custom" class="profile-avatar-custom-preview">';
376
+ uploadBtn.classList.add('is-active');
414
377
  }
378
+ var imprintButton = popoverEl && popoverEl.querySelector('.profile-use-imprint');
379
+ if (imprintButton) imprintButton.classList.remove('is-active');
415
380
  debouncedSave();
416
381
  }
417
382
  });
@@ -431,45 +396,25 @@ function showPopover() {
431
396
  });
432
397
  }
433
398
 
434
- // Avatar style — clicking confirms both the style and the current previewSeed
435
- popoverEl.querySelectorAll('.profile-avatar-option[data-style]').forEach(function(btn) {
436
- btn.addEventListener('click', function() {
437
- profile.avatarCustom = '';
438
- profile.avatarStyle = btn.dataset.style;
439
- profile.avatarSeed = previewSeed;
440
- applyToIsland();
441
- updatePopoverHeader();
442
- popoverEl.querySelectorAll('.profile-avatar-option').forEach(function(b) {
443
- b.classList.remove('profile-avatar-option-active');
444
- });
445
- btn.classList.add('profile-avatar-option-active');
446
- // Reset upload button to + icon
447
- var upBtn = popoverEl.querySelector('.profile-avatar-upload');
448
- if (upBtn) upBtn.innerHTML = '<span class="profile-avatar-upload-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></span>';
449
- debouncedSave();
450
- });
451
- });
452
-
453
- // Shuffle button — only changes preview candidates, not the actual profile
454
- popoverEl.querySelector('.profile-shuffle-btn').addEventListener('click', function(e) {
455
- e.stopPropagation();
456
- previewSeed = Math.random().toString(36).substring(2, 10);
457
- refreshAvatarPreviews();
399
+ popoverEl.querySelector('.profile-use-imprint').addEventListener('click', function() {
400
+ profile.avatarCustom = '';
401
+ applyToIsland();
402
+ updatePopoverHeader();
403
+ popoverEl.querySelector('.profile-use-imprint').classList.add('is-active');
404
+ popoverEl.querySelector('.profile-avatar-upload').classList.remove('is-active');
405
+ debouncedSave();
458
406
  });
459
407
 
460
- // Color swatches
461
- popoverEl.querySelectorAll('.profile-color-swatch').forEach(function(btn) {
462
- btn.addEventListener('click', function() {
463
- profile.avatarColor = btn.dataset.color;
464
- applyToIsland();
465
- var bannerEl = popoverEl.querySelector('.profile-banner');
466
- if (bannerEl) bannerEl.style.background = profile.avatarColor;
467
- popoverEl.querySelectorAll('.profile-color-swatch').forEach(function(b) {
468
- b.classList.remove('profile-color-active');
469
- });
470
- btn.classList.add('profile-color-active');
471
- debouncedSave();
472
- });
408
+ popoverEl.querySelector('.profile-regenerate-imprint').addEventListener('click', function() {
409
+ profile.avatarCustom = '';
410
+ profile.avatarStyle = 'imprint';
411
+ profile.avatarSeed = Math.random().toString(36).substring(2, 10);
412
+ applyToIsland();
413
+ updatePopoverHeader();
414
+ refreshImprintPreview(popoverEl, profile.avatarStyle, profile.avatarSeed);
415
+ popoverEl.querySelector('.profile-use-imprint').classList.add('is-active');
416
+ popoverEl.querySelector('.profile-avatar-upload').classList.remove('is-active');
417
+ debouncedSave();
473
418
  });
474
419
 
475
420
  // Prevent clicks inside popover from closing it
@@ -497,17 +442,15 @@ function updatePopoverHeader() {
497
442
  var nd = popoverEl.querySelector('.profile-name-display');
498
443
  if (img) img.src = profile.avatarCustom
499
444
  ? profile.avatarCustom
500
- : avatarUrl(profile.avatarStyle || 'thumbs', getAvatarSeed(), 80);
445
+ : avatarUrl(profile.avatarStyle || 'imprint', getAvatarSeed(), 80);
501
446
  if (nd) nd.textContent = profile.name || 'Awesome Clay User';
502
447
  }
503
448
 
504
449
 
505
- function refreshAvatarPreviews() {
506
- if (!popoverEl) return;
507
- popoverEl.querySelectorAll('.profile-avatar-option[data-style] img').forEach(function(img) {
508
- var style = img.closest('.profile-avatar-option').dataset.style;
509
- img.src = avatarUrl(style, previewSeed, 40);
510
- });
450
+ function refreshImprintPreview(root, style, seed) {
451
+ if (!root) return;
452
+ var img = root.querySelector('.profile-imprint-preview img');
453
+ if (img) img.src = avatarUrl(style || 'imprint', seed || 'anonymous', 72);
511
454
  }
512
455
 
513
456
  function closeOnOutside(e) {
@@ -610,7 +553,6 @@ export function getProfileLang() {
610
553
  // --- Mate profile popover (reuses same UI minus language) ---
611
554
  var matePopoverEl = null;
612
555
  var mateSaveTimer = null;
613
- var matePreviewSeed = '';
614
556
 
615
557
  export function showMateProfilePopover(anchorEl, mateData, onUpdate) {
616
558
  if (matePopoverEl) {
@@ -620,11 +562,10 @@ export function showMateProfilePopover(anchorEl, mateData, onUpdate) {
620
562
 
621
563
  var mp = mateData.profile || {};
622
564
  var mateName = mp.displayName || mateData.name || '';
623
- var mateColor = mp.avatarColor || '#7c3aed';
624
- var mateStyle = mp.avatarStyle || 'bottts';
565
+ var mateColor = mp.avatarColor || '#5857fc';
566
+ var mateStyle = mp.avatarStyle || 'imprint';
625
567
  var mateSeed = mp.avatarSeed || mateData.id || 'mate';
626
568
  var mateCustom = mp.avatarCustom || '';
627
- matePreviewSeed = mateSeed;
628
569
 
629
570
  matePopoverEl = document.createElement('div');
630
571
  matePopoverEl.className = 'profile-popover mate-profile-popover';
@@ -632,15 +573,15 @@ export function showMateProfilePopover(anchorEl, mateData, onUpdate) {
632
573
  var html = '';
633
574
 
634
575
  // Banner + close
635
- html += '<div class="profile-banner" style="background:' + mateColor + '">';
576
+ html += '<div class="profile-banner">';
636
577
  html += '<button class="profile-close-btn">&times;</button>';
637
578
  html += '</div>';
638
579
 
639
580
  // Avatar row
640
- var mateAvatarSrc = mateCustom ? mateCustom : avatarUrl(mateStyle, mateSeed, 80);
581
+ var mateAvatarSrc = mateCustom ? mateCustom : mateMarkUrl(mateName || 'Mate', 80);
641
582
  html += '<div class="profile-avatar-row">';
642
583
  html += '<div class="profile-popover-avatar">';
643
- html += '<img class="profile-popover-avatar-img" src="' + mateAvatarSrc + '" alt="avatar">';
584
+ html += '<img class="profile-popover-avatar-img" src="' + mateAvatarSrc + '" alt="' + escapeAttr(mateName || 'Clay Mate') + '">';
644
585
  html += '</div>';
645
586
  html += '<div class="profile-name-display">' + escapeAttr(mateName || 'New Mate') + '</div>';
646
587
  html += '</div>';
@@ -658,40 +599,17 @@ export function showMateProfilePopover(anchorEl, mateData, onUpdate) {
658
599
  var mateAvatarLocked = mp.avatarLocked || false;
659
600
  if (!mateAvatarLocked) {
660
601
  html += '<div class="profile-field">';
661
- html += '<label class="profile-field-label">Avatar <button class="profile-shuffle-btn" title="Shuffle">' + iconHtml('shuffle') + '</button></label>';
662
- html += '<div class="profile-avatar-grid">';
663
- // Upload button as first cell
664
- var mateUploadActive = mateCustom ? ' profile-avatar-option-active' : '';
665
- html += '<button class="profile-avatar-option profile-avatar-upload' + mateUploadActive + '" title="Upload photo">';
666
- if (mateCustom) {
667
- html += '<img src="' + mateCustom + '" alt="Custom" class="profile-avatar-custom-preview">';
668
- } else {
669
- html += '<span class="profile-avatar-upload-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></span>';
670
- }
671
- html += '</button>';
672
- html += '<input type="file" id="mate-profile-avatar-file" accept="image/*" style="display:none">';
673
- for (var j = 0; j < AVATAR_STYLES.length; j++) {
674
- var st = AVATAR_STYLES[j];
675
- var activeS = (!mateCustom && mateStyle === st.id) ? ' profile-avatar-option-active' : '';
676
- html += '<button class="profile-avatar-option' + activeS + '" data-style="' + st.id + '" title="' + st.name + '">';
677
- html += '<img src="' + avatarUrl(st.id, mateSeed, 40) + '" alt="' + st.name + '">';
678
- html += '</button>';
679
- }
680
- html += '</div>';
602
+ html += '<label class="profile-field-label">Identity</label>';
603
+ html += '<div class="profile-imprint-panel">';
604
+ html += '<div class="profile-imprint-preview"><img src="' + mateMarkUrl(mateName || 'Mate', 72) + '" alt="Clay Mate initial"><span><strong>Mate initial</strong><small>Based on display name</small></span></div>';
605
+ html += '<div class="profile-imprint-actions profile-mate-mark-actions">';
606
+ html += '<button class="profile-use-imprint' + (!mateCustom ? ' is-active' : '') + '" type="button">Use initial</button>';
607
+ html += '<button class="profile-avatar-upload' + (mateCustom ? ' is-active' : '') + '" type="button">Upload image</button>';
681
608
  html += '</div>';
682
- }
683
-
684
- // Color
685
- html += '<div class="profile-field">';
686
- html += '<label class="profile-field-label">Color</label>';
687
- html += '<div class="profile-color-grid">';
688
- for (var k = 0; k < COLORS.length; k++) {
689
- var c = COLORS[k];
690
- var activeC = (mateColor === c) ? ' profile-color-active' : '';
691
- html += '<button class="profile-color-swatch' + activeC + '" data-color="' + c + '" style="background:' + c + '"></button>';
692
- }
693
609
  html += '</div>';
610
+ html += '<input type="file" id="mate-profile-avatar-file" accept="image/*" style="display:none">';
694
611
  html += '</div>';
612
+ }
695
613
 
696
614
  html += '</div>'; // close body
697
615
 
@@ -746,7 +664,9 @@ export function showMateProfilePopover(anchorEl, mateData, onUpdate) {
746
664
  var nd = matePopoverEl.querySelector('.profile-name-display');
747
665
  if (img) img.src = mateProfile.avatarCustom
748
666
  ? mateProfile.avatarCustom
749
- : avatarUrl(mateProfile.avatarStyle, mateProfile.avatarSeed, 80);
667
+ : mateMarkUrl(mateProfile.displayName || 'Mate', 80);
668
+ var preview = matePopoverEl.querySelector('.profile-imprint-preview img');
669
+ if (preview) preview.src = mateMarkUrl(mateProfile.displayName || 'Mate', 72);
750
670
  if (nd) nd.textContent = mateProfile.displayName || 'New Mate';
751
671
  }
752
672
 
@@ -791,15 +711,11 @@ export function showMateProfilePopover(anchorEl, mateData, onUpdate) {
791
711
  if (data.ok) {
792
712
  mateProfile.avatarCustom = data.avatar;
793
713
  updateMatePopoverHeader();
794
- if (matePopoverEl) {
795
- matePopoverEl.querySelectorAll('.profile-avatar-option').forEach(function(b) {
796
- b.classList.remove('profile-avatar-option-active');
797
- });
798
- }
799
714
  if (mateUploadBtn) {
800
- mateUploadBtn.classList.add('profile-avatar-option-active');
801
- mateUploadBtn.innerHTML = '<img src="' + data.avatar + '" alt="Custom" class="profile-avatar-custom-preview">';
715
+ mateUploadBtn.classList.add('is-active');
802
716
  }
717
+ var imprintButton = matePopoverEl && matePopoverEl.querySelector('.profile-use-imprint');
718
+ if (imprintButton) imprintButton.classList.remove('is-active');
803
719
  debouncedMateUpdate();
804
720
  }
805
721
  });
@@ -819,48 +735,15 @@ export function showMateProfilePopover(anchorEl, mateData, onUpdate) {
819
735
  });
820
736
  }
821
737
 
822
- // Avatar style
823
- matePopoverEl.querySelectorAll('.profile-avatar-option[data-style]').forEach(function(btn) {
824
- btn.addEventListener('click', function() {
738
+ if (!mateAvatarLocked) {
739
+ matePopoverEl.querySelector('.profile-use-imprint').addEventListener('click', function() {
825
740
  mateProfile.avatarCustom = '';
826
- mateProfile.avatarStyle = btn.dataset.style;
827
- mateProfile.avatarSeed = matePreviewSeed;
828
741
  updateMatePopoverHeader();
829
- matePopoverEl.querySelectorAll('.profile-avatar-option').forEach(function(b) {
830
- b.classList.remove('profile-avatar-option-active');
831
- });
832
- btn.classList.add('profile-avatar-option-active');
833
- // Reset upload button to + icon
834
- var upBtn = matePopoverEl.querySelector('.profile-avatar-upload');
835
- if (upBtn) upBtn.innerHTML = '<span class="profile-avatar-upload-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></span>';
742
+ matePopoverEl.querySelector('.profile-use-imprint').classList.add('is-active');
743
+ matePopoverEl.querySelector('.profile-avatar-upload').classList.remove('is-active');
836
744
  debouncedMateUpdate();
837
745
  });
838
- });
839
-
840
- // Shuffle
841
- matePopoverEl.querySelector('.profile-shuffle-btn').addEventListener('click', function(e) {
842
- e.stopPropagation();
843
- matePreviewSeed = Math.random().toString(36).substring(2, 10);
844
- if (!matePopoverEl) return;
845
- matePopoverEl.querySelectorAll('.profile-avatar-option[data-style] img').forEach(function(img) {
846
- var style = img.closest('.profile-avatar-option').dataset.style;
847
- img.src = avatarUrl(style, matePreviewSeed, 40);
848
- });
849
- });
850
-
851
- // Color swatches
852
- matePopoverEl.querySelectorAll('.profile-color-swatch').forEach(function(btn) {
853
- btn.addEventListener('click', function() {
854
- mateProfile.avatarColor = btn.dataset.color;
855
- var bannerEl = matePopoverEl.querySelector('.profile-banner');
856
- if (bannerEl) bannerEl.style.background = mateProfile.avatarColor;
857
- matePopoverEl.querySelectorAll('.profile-color-swatch').forEach(function(b) {
858
- b.classList.remove('profile-color-active');
859
- });
860
- btn.classList.add('profile-color-active');
861
- debouncedMateUpdate();
862
- });
863
- });
746
+ }
864
747
 
865
748
  matePopoverEl.addEventListener('click', function(e) { e.stopPropagation(); });
866
749
 
@@ -29,13 +29,14 @@ function positionMenu() {
29
29
  var rect = button.getBoundingClientRect();
30
30
  var width = menu.offsetWidth;
31
31
  menu.style.top = Math.round(rect.bottom + 6) + "px";
32
- menu.style.left = Math.max(8, Math.round(rect.right - width)) + "px";
32
+ menu.style.left = Math.max(8, Math.min(window.innerWidth - width - 8, Math.round(rect.right - width))) + "px";
33
33
  }
34
34
 
35
35
  function actionRow(icon, label, description) {
36
36
  var row = document.createElement("button");
37
37
  row.type = "button";
38
38
  row.className = "session-actions-row";
39
+ row.setAttribute("role", "menuitem");
39
40
  row.innerHTML = iconHtml(icon, "session-actions-row-icon") +
40
41
  '<span class="session-actions-row-copy"><strong></strong><span></span></span>' +
41
42
  iconHtml("chevron-right", "session-actions-row-chevron");
@@ -217,7 +218,8 @@ function showHandoffDialog(options) {
217
218
  function showMainMenu() {
218
219
  closeMenu();
219
220
  var el = menuShell("Session actions");
220
- var addWorker = actionRow("bot", "Add AI Worker", "Open a second agent beside this session.");
221
+ var addWorker = actionRow("bot", "Add AI worker", "Open a second agent beside this session.");
222
+ addWorker.classList.add("is-primary");
221
223
  addWorker.addEventListener("click", function () {
222
224
  var sessionId = store.get('activeSessionId');
223
225
  if (!sessionId) return;
@@ -228,7 +230,7 @@ function showMainMenu() {
228
230
  vendor: store.get('currentVendor') || "claude",
229
231
  });
230
232
  });
231
- var handoff = actionRow("forward", "Continue in another agent", "Carry this conversation into a new session.");
233
+ var handoff = actionRow("send", "Send context to another agent", "Start a new session with this conversation's context.");
232
234
  var unavailable = !store.get('sessionHasHistory') || store.get('sessionIsProcessing');
233
235
  handoff.disabled = unavailable;
234
236
  handoff.title = unavailable ? "Available after the current turn is complete" : "";
@@ -262,7 +262,7 @@ export function renderSidebarPresence(onlineUsers) {
262
262
  // Presence avatar URL helper
263
263
  function presenceAvatarUrl(userOrStyle) {
264
264
  if (userOrStyle && typeof userOrStyle === "object") return userAvatarUrl(userOrStyle, 24);
265
- return userAvatarUrl({ avatarStyle: userOrStyle || "thumbs" }, 24);
265
+ return userAvatarUrl({ avatarStyle: userOrStyle || "imprint" }, 24);
266
266
  }
267
267
 
268
268
  // renderUserStrip: call with no args to read from store (subscriber pattern),
@@ -424,7 +424,7 @@ export function renderUserStrip(allUsers, onlineUserIds, myUserId, dmFavorites,
424
424
  avatar.className = "icon-strip-user-avatar" + (mate.primary ? " icon-strip-primary-mate" : "");
425
425
  avatar.src = mateAvatarUrl(mate, 34);
426
426
  avatar.alt = mp.displayName || mate.name || "Mate";
427
- var mateColor = (mp.avatarColor) || mate.avatarColor || "#7c3aed";
427
+ var mateColor = (mp.avatarColor) || mate.avatarColor || "#5857fc";
428
428
  avatar.style.background = mateColor + "30";
429
429
  el.appendChild(avatar);
430
430
 
@@ -679,7 +679,7 @@ function toggleDmUserPicker(anchorEl) {
679
679
  bItem.style.opacity = "0.7";
680
680
  var bAv = document.createElement("img");
681
681
  bAv.className = "dm-user-picker-avatar";
682
- bAv.src = mateAvatarUrl({ avatarCustom: b.avatarCustom, avatarStyle: b.avatarStyle || "bottts", avatarSeed: b.displayName, id: b.key }, 28);
682
+ bAv.src = mateAvatarUrl({ avatarCustom: b.avatarCustom, avatarStyle: b.avatarStyle || "imprint", avatarSeed: b.displayName, id: b.key, avatarColor: b.avatarColor }, 28);
683
683
  bAv.alt = b.displayName;
684
684
  bItem.appendChild(bAv);
685
685
  var bNameWrap = document.createElement("div");
@@ -866,7 +866,8 @@ function toggleDmUserPicker(anchorEl) {
866
866
  } else {
867
867
  avEl.src = mateAvatarUrl({
868
868
  avatarCustom: src.avatarCustom,
869
- avatarStyle: src.avatarStyle || "bottts",
869
+ avatarStyle: src.avatarStyle || "imprint",
870
+ avatarColor: src.avatarColor,
870
871
  avatarSeed: src.displayName,
871
872
  id: src.key,
872
873
  }, 32);
@@ -1543,7 +1543,7 @@ export function updateSessionPresence(presence) {
1543
1543
 
1544
1544
  function presenceAvatarUrl(userOrStyle, seed) {
1545
1545
  if (userOrStyle && typeof userOrStyle === "object") return userAvatarUrl(userOrStyle, 24);
1546
- return avatarUrl(userOrStyle || "thumbs", seed, 24);
1546
+ return avatarUrl(userOrStyle || "imprint", seed, 24);
1547
1547
  }
1548
1548
 
1549
1549
  function renderPresenceAvatars(el, sessionId) {
@@ -8,7 +8,7 @@
8
8
  @import url("css/overlays.css?v=20260827-brand3");
9
9
  @import url("css/menus.css");
10
10
  @import url("css/messages.css?v=20260828-delegated-muted1");
11
- @import url("css/generated-images.css?v=20260829c");
11
+ @import url("css/generated-images.css?v=20260829-save-details1");
12
12
  @import url("css/rewind.css");
13
13
  @import url("css/input.css?v=20260828-composer-depth1");
14
14
  @import url("css/filebrowser.css?v=20260829-terminal-canvas1");
@@ -38,3 +38,4 @@
38
38
  @import url("css/notifications-center.css");
39
39
  @import url("css/tui-attention.css?v=20260829-terminal-canvas1");
40
40
  @import url("css/pwa-mobile.css?v=20260824f");
41
+ @import url("css/avatar-imprints.css?v=20260829-imprints1");
@@ -414,8 +414,8 @@ function attachAuth(ctx) {
414
414
  adminProfile = {
415
415
  name: data.displayName || data.username,
416
416
  lang: existingProfile.lang || "en-US",
417
- avatarColor: existingProfile.avatarColor || "#7c3aed",
418
- avatarStyle: existingProfile.avatarStyle || "thumbs",
417
+ avatarColor: existingProfile.avatarColor || "#5857fc",
418
+ avatarStyle: existingProfile.avatarStyle || "imprint",
419
419
  avatarSeed: existingProfile.avatarSeed || crypto.randomBytes(4).toString("hex"),
420
420
  };
421
421
  } catch (e) {}