clay-server 2.34.1-beta.3 → 2.35.0-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.
- package/lib/daemon.js +8 -0
- package/lib/public/app.js +8 -1
- package/lib/public/css/mates.css +103 -0
- package/lib/public/index.html +18 -0
- package/lib/public/modules/app-messages.js +15 -1
- package/lib/public/modules/app-skills-install.js +72 -21
- package/lib/public/modules/mate-wizard.js +7 -0
- package/lib/public/modules/mention.js +9 -0
- package/lib/public/modules/profile.js +8 -0
- package/lib/public/modules/sidebar-mates.js +178 -65
- package/lib/public/modules/user-settings.js +50 -2
- package/lib/server-settings.js +46 -0
- package/lib/users-preferences.js +32 -0
- package/lib/users.js +4 -0
- package/package.json +1 -1
package/lib/daemon.js
CHANGED
|
@@ -681,6 +681,7 @@ var relay = createServer({
|
|
|
681
681
|
keepAwake: !!config.keepAwake,
|
|
682
682
|
autoContinueOnRateLimit: !!config.autoContinueOnRateLimit,
|
|
683
683
|
chatLayout: config.chatLayout || "channel",
|
|
684
|
+
matesEnabled: config.matesEnabled !== false,
|
|
684
685
|
pinEnabled: !!config.pinHash,
|
|
685
686
|
platform: process.platform,
|
|
686
687
|
hostname: os2.hostname(),
|
|
@@ -739,6 +740,13 @@ var relay = createServer({
|
|
|
739
740
|
console.log("[daemon] Auto-continue on rate limit:", want, "(web)");
|
|
740
741
|
return { ok: true, autoContinueOnRateLimit: want };
|
|
741
742
|
},
|
|
743
|
+
onSetMatesEnabled: function (value) {
|
|
744
|
+
var want = !!value;
|
|
745
|
+
config.matesEnabled = want;
|
|
746
|
+
saveConfig(config);
|
|
747
|
+
console.log("[daemon] Mates UI:", want ? "on" : "off", "(web)");
|
|
748
|
+
return { ok: true, matesEnabled: want };
|
|
749
|
+
},
|
|
742
750
|
onGetToolPalettes: function () {
|
|
743
751
|
return config.toolPalettes || {};
|
|
744
752
|
},
|
package/lib/public/app.js
CHANGED
|
@@ -285,6 +285,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
285
285
|
cachedMatesList: [],
|
|
286
286
|
cachedDmFavorites: [],
|
|
287
287
|
cachedAvailableBuiltins: [],
|
|
288
|
+
matesEnabled: true,
|
|
288
289
|
returningFromMateDm: false,
|
|
289
290
|
pendingMateInterview: null,
|
|
290
291
|
pendingTermCommand: null,
|
|
@@ -788,7 +789,13 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
788
789
|
get projectList() { return getCachedProjects(); },
|
|
789
790
|
});
|
|
790
791
|
fetch("/api/me").then(function (r) { return r.json(); }).then(function (d) {
|
|
791
|
-
if (d.multiUser) {
|
|
792
|
+
if (d.multiUser) {
|
|
793
|
+
store.set({ isMultiUserMode: true });
|
|
794
|
+
// Body class lets CSS treat single-user / multi-user differently for
|
|
795
|
+
// the Mates UI gate: single-user collapses the whole mate section,
|
|
796
|
+
// multi-user keeps it visible (still hosts other users' avatars).
|
|
797
|
+
if (document.body) document.body.classList.add('is-multi-user');
|
|
798
|
+
}
|
|
792
799
|
if (d.user && d.user.id) { store.set({ myUserId: d.user.id }); }
|
|
793
800
|
if (d.permissions) store.set({ permissions: d.permissions });
|
|
794
801
|
if (d.mustChangePin) showForceChangePinOverlay();
|
package/lib/public/css/mates.css
CHANGED
|
@@ -3104,3 +3104,106 @@ body.mate-dm-active .activity-inline:not(.mention-activity-bar):not(.mate-pre-ac
|
|
|
3104
3104
|
flex: 1;
|
|
3105
3105
|
}
|
|
3106
3106
|
}
|
|
3107
|
+
|
|
3108
|
+
/* === Mates UI gate === */
|
|
3109
|
+
/* body.mates-disabled is set when the user flips the Mates toggle off in
|
|
3110
|
+
User Settings → Mates. body.is-multi-user is set when the deploy hosts
|
|
3111
|
+
more than one user. Single-user collapses the whole Mates surface;
|
|
3112
|
+
multi-user keeps Mates-adjacent containers visible (they still host
|
|
3113
|
+
other users) but greys out and disables the Mates-specific bits. */
|
|
3114
|
+
|
|
3115
|
+
/* Always hide pure-Mates UI: rotating switch hint, individual mate
|
|
3116
|
+
avatars in the icon strip, the home-hub mates strip, and the
|
|
3117
|
+
"Ask Mate" button in the chat input. These never contain anything
|
|
3118
|
+
but mates, so they collapse in both modes. */
|
|
3119
|
+
body.mates-disabled .icon-strip-mate,
|
|
3120
|
+
body.mates-disabled #icon-strip-hint-mate,
|
|
3121
|
+
body.mates-disabled #home-hub-mates,
|
|
3122
|
+
body.mates-disabled #ask-mate-btn {
|
|
3123
|
+
display: none !important;
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
/* Single-user only: collapse the whole icon-strip mate wrapper since it
|
|
3127
|
+
shares space with the user-strip but has no other users to display in
|
|
3128
|
+
single-user mode. The DM picker stays visible in both modes so the
|
|
3129
|
+
user always has a way to discover the toggle (single-user can't reach
|
|
3130
|
+
the picker today since the + button lives inside the wrapper, but if
|
|
3131
|
+
that ever changes the same disabled UX applies). */
|
|
3132
|
+
body.mates-disabled:not(.is-multi-user) #icon-strip-mate-section {
|
|
3133
|
+
display: none !important;
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3136
|
+
/* === Mates discovery promo (DM picker, disabled state) === */
|
|
3137
|
+
/* Shown in both modes when Mates is off. Replaces the old "greyed mate
|
|
3138
|
+
list with trash icons" pattern with a softer invitation: section
|
|
3139
|
+
label, an animated avatar marquee that loops left, a value-prop
|
|
3140
|
+
sentence, and a CTA that deep-links into User Settings → Mates. */
|
|
3141
|
+
|
|
3142
|
+
.dm-picker-mates-promo {
|
|
3143
|
+
padding: 0 12px 8px;
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
.dm-picker-mates-promo .dm-user-picker-section {
|
|
3147
|
+
margin-top: 0;
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
.dm-picker-mates-marquee {
|
|
3151
|
+
position: relative;
|
|
3152
|
+
height: 28px;
|
|
3153
|
+
margin: 4px -12px;
|
|
3154
|
+
overflow: hidden;
|
|
3155
|
+
-webkit-mask-image: linear-gradient(to right, transparent, black 12%, black 88%, transparent);
|
|
3156
|
+
mask-image: linear-gradient(to right, transparent, black 12%, black 88%, transparent);
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
.dm-picker-mates-marquee-track {
|
|
3160
|
+
display: flex;
|
|
3161
|
+
align-items: center;
|
|
3162
|
+
gap: 8px;
|
|
3163
|
+
width: max-content;
|
|
3164
|
+
height: 100%;
|
|
3165
|
+
padding: 0 12px;
|
|
3166
|
+
animation: dm-picker-mates-marquee-flow 22s linear infinite;
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
.dm-picker-mates-marquee-avatar {
|
|
3170
|
+
width: 24px;
|
|
3171
|
+
height: 24px;
|
|
3172
|
+
border-radius: 50%;
|
|
3173
|
+
flex-shrink: 0;
|
|
3174
|
+
opacity: 0.85;
|
|
3175
|
+
background: var(--bg-alt, rgba(255, 255, 255, 0.04));
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
@keyframes dm-picker-mates-marquee-flow {
|
|
3179
|
+
from { transform: translateX(0); }
|
|
3180
|
+
to { transform: translateX(-50%); }
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
.dm-picker-mates-promo-text {
|
|
3184
|
+
font-size: 11px;
|
|
3185
|
+
color: var(--text-dimmer, #888);
|
|
3186
|
+
line-height: 1.5;
|
|
3187
|
+
margin: 4px 0 8px;
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
.dm-picker-mates-promo-cta {
|
|
3191
|
+
display: flex;
|
|
3192
|
+
align-items: center;
|
|
3193
|
+
justify-content: center;
|
|
3194
|
+
gap: 6px;
|
|
3195
|
+
width: 100%;
|
|
3196
|
+
padding: 6px 10px;
|
|
3197
|
+
border: 1px solid var(--border, #2a2a2a);
|
|
3198
|
+
background: var(--bg-alt, rgba(255, 255, 255, 0.04));
|
|
3199
|
+
color: var(--text);
|
|
3200
|
+
font-size: 11px;
|
|
3201
|
+
cursor: pointer;
|
|
3202
|
+
border-radius: 6px;
|
|
3203
|
+
transition: background 0.15s, border-color 0.15s;
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3206
|
+
.dm-picker-mates-promo-cta:hover {
|
|
3207
|
+
background: var(--bg-hover, rgba(255, 255, 255, 0.08));
|
|
3208
|
+
border-color: var(--accent, #6366f1);
|
|
3209
|
+
}
|
package/lib/public/index.html
CHANGED
|
@@ -909,12 +909,14 @@
|
|
|
909
909
|
<option value="us-account" selected>Account</option>
|
|
910
910
|
<option value="us-appearance">Appearance</option>
|
|
911
911
|
<option value="us-chat">Chat</option>
|
|
912
|
+
<option value="us-mates">Mates</option>
|
|
912
913
|
<option value="us-email">Email</option>
|
|
913
914
|
</select>
|
|
914
915
|
<div class="us-modal-nav-items">
|
|
915
916
|
<button class="us-nav-item active" data-section="us-account"><span>Account</span></button>
|
|
916
917
|
<button class="us-nav-item" data-section="us-appearance"><span>Appearance</span></button>
|
|
917
918
|
<button class="us-nav-item" data-section="us-chat"><span>Chat</span></button>
|
|
919
|
+
<button class="us-nav-item" data-section="us-mates"><span>Mates</span></button>
|
|
918
920
|
<button class="us-nav-item" data-section="us-email"><span>Email</span></button>
|
|
919
921
|
<div class="us-nav-separator"></div>
|
|
920
922
|
<button class="us-nav-item us-nav-danger" data-section="us-logout"><span>Log Out</span></button>
|
|
@@ -1037,6 +1039,22 @@
|
|
|
1037
1039
|
</div>
|
|
1038
1040
|
</div>
|
|
1039
1041
|
|
|
1042
|
+
<!-- Mates section -->
|
|
1043
|
+
<div class="us-section" data-section="us-mates">
|
|
1044
|
+
<h2>Mates</h2>
|
|
1045
|
+
<p class="settings-hint">Specialist AI teammates with assigned roles and long-term memory across sessions. They carry context forward and push back when a session drifts. Use the built-ins or design your own.</p>
|
|
1046
|
+
<div class="settings-card">
|
|
1047
|
+
<label class="settings-toggle-row">
|
|
1048
|
+
<div>
|
|
1049
|
+
<span class="settings-label">Use Mates</span>
|
|
1050
|
+
<div class="settings-hint">Off hides every Mates surface in Clay. Existing Mates and their memory are preserved.</div>
|
|
1051
|
+
</div>
|
|
1052
|
+
<input type="checkbox" id="us-mates-enabled">
|
|
1053
|
+
<span class="toggle-track"><span class="toggle-thumb"></span></span>
|
|
1054
|
+
</label>
|
|
1055
|
+
</div>
|
|
1056
|
+
</div>
|
|
1057
|
+
|
|
1040
1058
|
<!-- Email section -->
|
|
1041
1059
|
<div class="us-section" data-section="us-email">
|
|
1042
1060
|
<h2>Email Accounts</h2>
|
|
@@ -356,6 +356,16 @@ export function processMessage(msg) {
|
|
|
356
356
|
break;
|
|
357
357
|
|
|
358
358
|
case "model_info": {
|
|
359
|
+
// Drop stale model_info from a vendor that doesn't match the active
|
|
360
|
+
// session's vendor. On high-latency connections, the server's default-
|
|
361
|
+
// adapter model_info can arrive after session_switched has already
|
|
362
|
+
// bound the session to a different vendor. Applying it would replace
|
|
363
|
+
// currentModels with the wrong vendor's list and trigger app-panels
|
|
364
|
+
// to request models for the "wrong" vendor, which feeds back into a
|
|
365
|
+
// ping-pong loop of vendor flapping. See issue #336.
|
|
366
|
+
var _curV = store.get('currentVendor');
|
|
367
|
+
if (msg.vendor && _curV && msg.vendor !== _curV) break;
|
|
368
|
+
|
|
359
369
|
var _modelVal = msg.model;
|
|
360
370
|
if (_modelVal && typeof _modelVal === "object") _modelVal = _modelVal.value || _modelVal.displayName || "";
|
|
361
371
|
var _miUpdate = { currentModels: msg.models || [] };
|
|
@@ -530,8 +540,12 @@ export function processMessage(msg) {
|
|
|
530
540
|
if (!store.get('vendorSelectionLocked') || msg.hasHistory) {
|
|
531
541
|
store.set({ currentVendor: msg.vendor });
|
|
532
542
|
}
|
|
543
|
+
// Sessions with history have their vendor structurally bound to
|
|
544
|
+
// the session: lock so a late-arriving default-adapter model_info
|
|
545
|
+
// can't flip the UI back. Previously this branch unlocked, which
|
|
546
|
+
// is what allowed the feedback loop in issue #336.
|
|
533
547
|
if (msg.hasHistory) {
|
|
534
|
-
store.set({ vendorSelectionLocked:
|
|
548
|
+
store.set({ vendorSelectionLocked: true });
|
|
535
549
|
}
|
|
536
550
|
} else if (msg.hasHistory) {
|
|
537
551
|
// Existing session without explicit vendor: reset to claude
|
|
@@ -18,6 +18,11 @@ var pendingSkillInstalls = [];
|
|
|
18
18
|
var skillInstallCallback = null;
|
|
19
19
|
var skillInstalling = false;
|
|
20
20
|
var skillInstallDone = false;
|
|
21
|
+
// True when the modal contains only "outdated" skills (no "missing"). In that
|
|
22
|
+
// case the user is allowed to skip the update and continue with the original
|
|
23
|
+
// action; the dismissal is remembered for the rest of the browser session so
|
|
24
|
+
// we don't re-prompt on every reconnect / DM open.
|
|
25
|
+
var skillInstallSkippable = false;
|
|
21
26
|
|
|
22
27
|
export function initSkillInstall() {
|
|
23
28
|
skillInstallModal = document.getElementById("skill-install-modal");
|
|
@@ -28,8 +33,8 @@ export function initSkillInstall() {
|
|
|
28
33
|
skillInstallCancel = document.getElementById("skill-install-cancel");
|
|
29
34
|
skillInstallStatus = document.getElementById("skill-install-status");
|
|
30
35
|
|
|
31
|
-
skillInstallCancel.addEventListener("click",
|
|
32
|
-
skillInstallModal.querySelector(".confirm-backdrop").addEventListener("click",
|
|
36
|
+
skillInstallCancel.addEventListener("click", onSkillInstallDismiss);
|
|
37
|
+
skillInstallModal.querySelector(".confirm-backdrop").addEventListener("click", onSkillInstallDismiss);
|
|
33
38
|
|
|
34
39
|
skillInstallOk.addEventListener("click", function () {
|
|
35
40
|
if (skillInstallDone) {
|
|
@@ -106,6 +111,11 @@ function renderSkillInstallDialog(opts, missing) {
|
|
|
106
111
|
if (hasMissing && hasOutdated) btnLabel = "Install / Update";
|
|
107
112
|
skillInstallOk.textContent = btnLabel;
|
|
108
113
|
skillInstallOk.className = "confirm-btn confirm-delete";
|
|
114
|
+
// When only outdated skills are pending, the feature still works on the
|
|
115
|
+
// current version. Let the user skip and continue; surface that as a
|
|
116
|
+
// distinct cancel label so it's clear this won't abort the action.
|
|
117
|
+
skillInstallSkippable = hasOutdated && !hasMissing;
|
|
118
|
+
skillInstallCancel.textContent = skillInstallSkippable ? "Skip" : "Cancel";
|
|
109
119
|
skillInstallModal.classList.remove("hidden");
|
|
110
120
|
}
|
|
111
121
|
|
|
@@ -115,6 +125,45 @@ function hideSkillInstallModal() {
|
|
|
115
125
|
pendingSkillInstalls = [];
|
|
116
126
|
skillInstalling = false;
|
|
117
127
|
skillInstallDone = false;
|
|
128
|
+
skillInstallSkippable = false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Cancel/backdrop click handler. For "missing" skills the feature genuinely
|
|
132
|
+
// cannot run without them, so dismiss = drop the callback (existing behavior).
|
|
133
|
+
// For "outdated"-only prompts the feature still works on the old version, so
|
|
134
|
+
// dismiss = remember the choice for this browser session and proceed with cb.
|
|
135
|
+
function onSkillInstallDismiss() {
|
|
136
|
+
if (skillInstallSkippable) {
|
|
137
|
+
rememberOutdatedDismissal(pendingSkillInstalls);
|
|
138
|
+
var proceedCb = skillInstallCallback;
|
|
139
|
+
skillInstallCallback = null;
|
|
140
|
+
hideSkillInstallModal();
|
|
141
|
+
if (proceedCb) proceedCb();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
hideSkillInstallModal();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function dismissalKey(name, remoteVersion) {
|
|
148
|
+
return "clay-skill-update-dismissed:" + name + ":" + (remoteVersion || "");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function rememberOutdatedDismissal(items) {
|
|
152
|
+
try {
|
|
153
|
+
for (var i = 0; i < items.length; i++) {
|
|
154
|
+
var it = items[i];
|
|
155
|
+
if (it.status !== "outdated") continue;
|
|
156
|
+
sessionStorage.setItem(dismissalKey(it.name, it.remoteVersion), "1");
|
|
157
|
+
}
|
|
158
|
+
} catch (e) {}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function isOutdatedDismissed(name, remoteVersion) {
|
|
162
|
+
try {
|
|
163
|
+
return sessionStorage.getItem(dismissalKey(name, remoteVersion)) === "1";
|
|
164
|
+
} catch (e) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
118
167
|
}
|
|
119
168
|
|
|
120
169
|
function updateSkillInstallProgress(done, total) {
|
|
@@ -197,29 +246,31 @@ export function requireSkills(opts, cb) {
|
|
|
197
246
|
.then(function (res) { return res.json(); })
|
|
198
247
|
.then(function (data) {
|
|
199
248
|
var results = data.results || [];
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
249
|
+
// "missing" skills hard-block: the feature cannot run without them.
|
|
250
|
+
// "outdated" skills are surfaced too because skills look like a single
|
|
251
|
+
// user-facing feature but call vendor-specific tools (codex vs claude)
|
|
252
|
+
// internally — version drift breaks that consistency. The modal is
|
|
253
|
+
// skippable for outdated-only cases, and a session-scoped dismissal
|
|
254
|
+
// suppresses re-prompts so reconnect/refresh doesn't re-fire it.
|
|
204
255
|
var actionable = [];
|
|
205
256
|
for (var i = 0; i < results.length; i++) {
|
|
206
257
|
var r = results[i];
|
|
207
|
-
if (r.status
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
if (!orig) continue;
|
|
213
|
-
actionable.push({
|
|
214
|
-
name: r.name,
|
|
215
|
-
url: orig.url,
|
|
216
|
-
scope: orig.scope || "global",
|
|
217
|
-
installed: false,
|
|
218
|
-
status: r.status,
|
|
219
|
-
installedVersion: r.installedVersion,
|
|
220
|
-
remoteVersion: r.remoteVersion,
|
|
221
|
-
});
|
|
258
|
+
if (r.status !== "missing" && r.status !== "outdated") continue;
|
|
259
|
+
if (r.status === "outdated" && isOutdatedDismissed(r.name, r.remoteVersion)) continue;
|
|
260
|
+
var orig = null;
|
|
261
|
+
for (var j = 0; j < opts.skills.length; j++) {
|
|
262
|
+
if (opts.skills[j].name === r.name) { orig = opts.skills[j]; break; }
|
|
222
263
|
}
|
|
264
|
+
if (!orig) continue;
|
|
265
|
+
actionable.push({
|
|
266
|
+
name: r.name,
|
|
267
|
+
url: orig.url,
|
|
268
|
+
scope: orig.scope || "global",
|
|
269
|
+
installed: false,
|
|
270
|
+
status: r.status,
|
|
271
|
+
installedVersion: r.installedVersion,
|
|
272
|
+
remoteVersion: r.remoteVersion,
|
|
273
|
+
});
|
|
223
274
|
}
|
|
224
275
|
if (actionable.length === 0) { cb(); return; }
|
|
225
276
|
pendingSkillInstalls = actionable;
|
|
@@ -102,6 +102,13 @@ export function initMateWizard(sendWs, onMateCreated) {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
export function openMateWizard() {
|
|
105
|
+
// Defensive: if the user has turned off the Mates UI in User Settings,
|
|
106
|
+
// do not open the wizard even if it gets fired programmatically (e.g.
|
|
107
|
+
// via a stale event handler). The CSS gate hides the entry point but
|
|
108
|
+
// we still short-circuit here so JS calls become no-ops.
|
|
109
|
+
if (typeof document !== "undefined" && document.body && document.body.classList.contains("mates-disabled")) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
105
112
|
mateWizardStep = 0;
|
|
106
113
|
mateWizardData = {
|
|
107
114
|
relationship: null,
|
|
@@ -54,6 +54,15 @@ export function checkForMention(value, cursorPos) {
|
|
|
54
54
|
|
|
55
55
|
// --- Autocomplete dropdown ---
|
|
56
56
|
export function showMentionMenu(query) {
|
|
57
|
+
// Mates UI gate: if the user has turned Mates off in User Settings,
|
|
58
|
+
// the @-mention dropdown is the entry point for sending a message to a
|
|
59
|
+
// mate from the regular session input, so suppress it entirely. Once
|
|
60
|
+
// we surface user-to-user DMs from this dropdown we can scope this
|
|
61
|
+
// check tighter, but for now the dropdown is mate-only.
|
|
62
|
+
if (store.get('matesEnabled') === false) {
|
|
63
|
+
hideMentionMenu();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
57
66
|
var mates = ctx.matesList ? ctx.matesList() : [];
|
|
58
67
|
if (!mates || mates.length === 0) {
|
|
59
68
|
hideMentionMenu();
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { iconHtml, refreshIcons } from './icons.js';
|
|
6
6
|
import { setSTTLang } from './stt.js';
|
|
7
7
|
import { avatarUrl, mateAvatarUrl, AVATAR_STYLES } from './avatar.js';
|
|
8
|
+
import { store } from './store.js';
|
|
8
9
|
|
|
9
10
|
var ctx;
|
|
10
11
|
var profile = { name: '', lang: 'en-US', avatarStyle: 'thumbs', avatarSeed: '', avatarColor: '#7c3aed', avatarCustom: '' };
|
|
@@ -577,6 +578,13 @@ export function initProfile(_ctx) {
|
|
|
577
578
|
if (profile.lang) {
|
|
578
579
|
setSTTLang(profile.lang);
|
|
579
580
|
}
|
|
581
|
+
|
|
582
|
+
// Apply Mates UI gate at boot so the sidebar avatars / DM picker
|
|
583
|
+
// entry / home-hub strip are hidden before the user opens settings.
|
|
584
|
+
// Default true: only flip the body class off when explicitly false.
|
|
585
|
+
var matesOn = data.matesEnabled !== false;
|
|
586
|
+
store.set({ matesEnabled: matesOn });
|
|
587
|
+
if (document.body) document.body.classList.toggle('mates-disabled', !matesOn);
|
|
580
588
|
}).catch(function(err) {
|
|
581
589
|
console.warn('[Profile] Failed to load:', err);
|
|
582
590
|
});
|
|
@@ -11,6 +11,7 @@ import { closeProjectCtxMenu } from './sidebar-projects.js';
|
|
|
11
11
|
import { spawnDustParticles } from './sidebar.js';
|
|
12
12
|
import { openDm } from './app-dm.js';
|
|
13
13
|
import { openMateWizard } from './mate-wizard.js';
|
|
14
|
+
import { openUserSettings } from './user-settings.js';
|
|
14
15
|
import { getCachedProjects } from './app-projects.js';
|
|
15
16
|
|
|
16
17
|
function sendWs(msg) {
|
|
@@ -376,11 +377,14 @@ export function renderUserStrip(allUsers, onlineUserIds, myUserId, dmFavorites,
|
|
|
376
377
|
}
|
|
377
378
|
}
|
|
378
379
|
|
|
379
|
-
// Render mates
|
|
380
|
-
|
|
380
|
+
// Render mates: only favorited or unread, regardless of mode. Previously
|
|
381
|
+
// single-user mode short-circuited to "show all", which meant users who
|
|
382
|
+
// never engaged with Mates still saw 6 permanent avatars in the rail
|
|
383
|
+
// with no way to thin them out (#341). Applying the same filter as
|
|
384
|
+
// multi-user lets users curate the icon strip via favorites; the full
|
|
385
|
+
// mate list is still reachable from the DM picker.
|
|
381
386
|
var favoriteMates = cachedMates.filter(function (m) {
|
|
382
387
|
if (cachedDmRemovedUsers[m.id]) return false;
|
|
383
|
-
if (singleUser) return true;
|
|
384
388
|
if (cachedDmFavorites.indexOf(m.id) !== -1) return true;
|
|
385
389
|
if (cachedDmUnread[m.id] && cachedDmUnread[m.id] > 0) return true;
|
|
386
390
|
return false;
|
|
@@ -511,11 +515,17 @@ function toggleDmUserPicker(anchorEl) {
|
|
|
511
515
|
picker.className = "dm-user-picker";
|
|
512
516
|
picker.id = "dm-user-picker";
|
|
513
517
|
|
|
518
|
+
// Mates enabled flag is consulted up here so the search input can drop
|
|
519
|
+
// mates from its placeholder copy when the section is hidden.
|
|
520
|
+
var matesEnabled = store.get('matesEnabled') !== false;
|
|
521
|
+
|
|
514
522
|
// Search input
|
|
515
523
|
var searchInput = document.createElement("input");
|
|
516
524
|
searchInput.className = "dm-user-picker-search";
|
|
517
525
|
searchInput.type = "text";
|
|
518
|
-
searchInput.placeholder =
|
|
526
|
+
searchInput.placeholder = matesEnabled
|
|
527
|
+
? "Search mates and users..."
|
|
528
|
+
: "Search users...";
|
|
519
529
|
picker.appendChild(searchInput);
|
|
520
530
|
|
|
521
531
|
// User list element (appended later, after USERS label)
|
|
@@ -575,35 +585,51 @@ function toggleDmUserPicker(anchorEl) {
|
|
|
575
585
|
}
|
|
576
586
|
}
|
|
577
587
|
|
|
578
|
-
// ---
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
//
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
588
|
+
// --- Layout depends on whether Mates is enabled ---
|
|
589
|
+
// When enabled: Mates section first, then Users. Mates list is fully
|
|
590
|
+
// rendered with delete buttons, create-mate entry, etc.
|
|
591
|
+
// When disabled: Users first, then a Mates discovery promo (animated
|
|
592
|
+
// avatar marquee + value prop + a single CTA that deep-links into
|
|
593
|
+
// User Settings → Mates). We deliberately don't render the actual
|
|
594
|
+
// mate list when off — showing real mates with greyed-out trash icons
|
|
595
|
+
// is visually heavy and contradicts the user's choice to hide them.
|
|
596
|
+
// (matesEnabled is computed earlier near the search input.)
|
|
597
|
+
|
|
598
|
+
// matesListEl + renderMatesList are only used in the enabled layout.
|
|
599
|
+
// Declared up-front so the search handler can reference renderMatesList
|
|
600
|
+
// safely; renderMatesList stays a no-op in disabled mode.
|
|
601
|
+
var matesListEl = null;
|
|
602
|
+
var renderMatesList = function () {};
|
|
603
|
+
|
|
604
|
+
function renderMatesEnabledSection() {
|
|
605
|
+
var matesSectionLabel = document.createElement("div");
|
|
606
|
+
matesSectionLabel.className = "dm-user-picker-section";
|
|
607
|
+
matesSectionLabel.textContent = "Mates";
|
|
608
|
+
picker.appendChild(matesSectionLabel);
|
|
609
|
+
|
|
610
|
+
matesListEl = document.createElement("div");
|
|
611
|
+
matesListEl.className = "dm-user-picker-list dm-mates-list";
|
|
612
|
+
picker.appendChild(matesListEl);
|
|
613
|
+
|
|
614
|
+
// Update scroll gradient hint
|
|
615
|
+
function updateMatesScrollHint() {
|
|
616
|
+
var isOverflow = matesListEl.scrollHeight > matesListEl.clientHeight + 2;
|
|
617
|
+
if (!isOverflow) {
|
|
618
|
+
matesListEl.classList.add("no-overflow");
|
|
619
|
+
matesListEl.classList.remove("scrolled-bottom");
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
matesListEl.classList.remove("no-overflow");
|
|
623
|
+
var atBottom = matesListEl.scrollTop + matesListEl.clientHeight >= matesListEl.scrollHeight - 4;
|
|
624
|
+
if (atBottom) {
|
|
625
|
+
matesListEl.classList.add("scrolled-bottom");
|
|
626
|
+
} else {
|
|
627
|
+
matesListEl.classList.remove("scrolled-bottom");
|
|
628
|
+
}
|
|
602
629
|
}
|
|
603
|
-
|
|
604
|
-
matesListEl.addEventListener("scroll", updateMatesScrollHint);
|
|
630
|
+
matesListEl.addEventListener("scroll", updateMatesScrollHint);
|
|
605
631
|
|
|
606
|
-
|
|
632
|
+
renderMatesList = function (filter) {
|
|
607
633
|
matesListEl.innerHTML = "";
|
|
608
634
|
var allMates = cachedMates || [];
|
|
609
635
|
if (filter) {
|
|
@@ -752,42 +778,129 @@ function toggleDmUserPicker(anchorEl) {
|
|
|
752
778
|
emptyEl.textContent = "No mates found";
|
|
753
779
|
matesListEl.appendChild(emptyEl);
|
|
754
780
|
}
|
|
755
|
-
|
|
756
|
-
|
|
781
|
+
refreshIcons();
|
|
782
|
+
requestAnimationFrame(updateMatesScrollHint);
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
// Create Mate option
|
|
786
|
+
var createMateEl = document.createElement("div");
|
|
787
|
+
createMateEl.className = "dm-user-picker-create-mate";
|
|
788
|
+
var hasCustomMates = (cachedMates || []).some(function (m) { return !m.builtinKey; });
|
|
789
|
+
var createMateLabel = hasCustomMates ? "Create a Mate" : "Create a Mate for what you're doing";
|
|
790
|
+
createMateEl.innerHTML = iconHtml("bot") + " <span>" + createMateLabel + "</span>";
|
|
791
|
+
createMateEl.addEventListener("click", function () {
|
|
792
|
+
closeDmUserPicker();
|
|
793
|
+
if (openMateWizard) openMateWizard();
|
|
794
|
+
});
|
|
795
|
+
picker.appendChild(createMateEl);
|
|
796
|
+
|
|
797
|
+
// Divider
|
|
798
|
+
var divider = document.createElement("div");
|
|
799
|
+
divider.style.borderTop = "1px solid var(--border, #333)";
|
|
800
|
+
divider.style.margin = "4px 0";
|
|
801
|
+
picker.appendChild(divider);
|
|
802
|
+
|
|
803
|
+
// Users section
|
|
804
|
+
var usersLabelEnabled = document.createElement("div");
|
|
805
|
+
usersLabelEnabled.className = "dm-user-picker-section";
|
|
806
|
+
usersLabelEnabled.textContent = "Users";
|
|
807
|
+
picker.appendChild(usersLabelEnabled);
|
|
808
|
+
picker.appendChild(listEl);
|
|
757
809
|
}
|
|
758
810
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
var
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
811
|
+
function renderMatesDisabledLayout() {
|
|
812
|
+
// Users section first when Mates is off — they are the only thing
|
|
813
|
+
// a user can actually act on, so they lead.
|
|
814
|
+
var usersLabel = document.createElement("div");
|
|
815
|
+
usersLabel.className = "dm-user-picker-section";
|
|
816
|
+
usersLabel.textContent = "Users";
|
|
817
|
+
picker.appendChild(usersLabel);
|
|
818
|
+
picker.appendChild(listEl);
|
|
819
|
+
|
|
820
|
+
// Divider
|
|
821
|
+
var divider = document.createElement("div");
|
|
822
|
+
divider.style.borderTop = "1px solid var(--border, #333)";
|
|
823
|
+
divider.style.margin = "8px 0 4px";
|
|
824
|
+
picker.appendChild(divider);
|
|
825
|
+
|
|
826
|
+
// Mates discovery promo: animated avatar marquee + value prop + CTA.
|
|
827
|
+
// The marquee uses real cached mate avatars purely as decoration; we
|
|
828
|
+
// never attach click handlers, never show delete buttons, and never
|
|
829
|
+
// render a list view — turning the section into a soft invitation
|
|
830
|
+
// rather than a "list with everything greyed out" tease.
|
|
831
|
+
var promo = document.createElement("div");
|
|
832
|
+
promo.className = "dm-picker-mates-promo";
|
|
833
|
+
|
|
834
|
+
var promoLabel = document.createElement("div");
|
|
835
|
+
promoLabel.className = "dm-user-picker-section";
|
|
836
|
+
promoLabel.textContent = "Mates";
|
|
837
|
+
promo.appendChild(promoLabel);
|
|
838
|
+
|
|
839
|
+
var marqueeWrap = document.createElement("div");
|
|
840
|
+
marqueeWrap.className = "dm-picker-mates-marquee";
|
|
841
|
+
var marqueeTrack = document.createElement("div");
|
|
842
|
+
marqueeTrack.className = "dm-picker-mates-marquee-track";
|
|
843
|
+
var marqueeSource = (cachedMates && cachedMates.length > 0)
|
|
844
|
+
? cachedMates.slice(0, 8)
|
|
845
|
+
: (store.get('cachedAvailableBuiltins') || []).slice(0, 6);
|
|
846
|
+
// Duplicate the avatar set so the keyframe loop wraps seamlessly.
|
|
847
|
+
for (var copy = 0; copy < 2; copy++) {
|
|
848
|
+
for (var mi = 0; mi < marqueeSource.length; mi++) {
|
|
849
|
+
var src = marqueeSource[mi];
|
|
850
|
+
var avEl = document.createElement("img");
|
|
851
|
+
avEl.className = "dm-picker-mates-marquee-avatar";
|
|
852
|
+
avEl.alt = "";
|
|
853
|
+
avEl.setAttribute("aria-hidden", "true");
|
|
854
|
+
if (src.id) {
|
|
855
|
+
avEl.src = mateAvatarUrl(src, 32);
|
|
856
|
+
} else {
|
|
857
|
+
avEl.src = mateAvatarUrl({
|
|
858
|
+
avatarCustom: src.avatarCustom,
|
|
859
|
+
avatarStyle: src.avatarStyle || "bottts",
|
|
860
|
+
avatarSeed: src.displayName,
|
|
861
|
+
id: src.key,
|
|
862
|
+
}, 32);
|
|
863
|
+
}
|
|
864
|
+
marqueeTrack.appendChild(avEl);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
marqueeWrap.appendChild(marqueeTrack);
|
|
868
|
+
promo.appendChild(marqueeWrap);
|
|
869
|
+
|
|
870
|
+
var promoText = document.createElement("div");
|
|
871
|
+
promoText.className = "dm-picker-mates-promo-text";
|
|
872
|
+
promoText.textContent = "Specialist AI teammates with long-term memory across sessions. Mention with @ for design, engineering, strategy, or marketing, or build your own.";
|
|
873
|
+
promo.appendChild(promoText);
|
|
874
|
+
|
|
875
|
+
var cta = document.createElement("button");
|
|
876
|
+
cta.type = "button";
|
|
877
|
+
cta.className = "dm-picker-mates-promo-cta";
|
|
878
|
+
cta.textContent = "Click here to enable Mates";
|
|
879
|
+
cta.addEventListener("click", function () {
|
|
880
|
+
closeDmUserPicker();
|
|
881
|
+
openUserSettings('us-mates');
|
|
882
|
+
});
|
|
883
|
+
promo.appendChild(cta);
|
|
884
|
+
|
|
885
|
+
picker.appendChild(promo);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
if (matesEnabled) {
|
|
889
|
+
renderMatesEnabledSection();
|
|
890
|
+
renderMatesList("");
|
|
891
|
+
renderPickerList("");
|
|
892
|
+
searchInput.addEventListener("input", function () {
|
|
893
|
+
var val = searchInput.value;
|
|
894
|
+
renderMatesList(val);
|
|
895
|
+
renderPickerList(val);
|
|
896
|
+
});
|
|
897
|
+
} else {
|
|
898
|
+
renderMatesDisabledLayout();
|
|
899
|
+
renderPickerList("");
|
|
900
|
+
searchInput.addEventListener("input", function () {
|
|
901
|
+
renderPickerList(searchInput.value);
|
|
902
|
+
});
|
|
903
|
+
}
|
|
791
904
|
|
|
792
905
|
// Focus search
|
|
793
906
|
setTimeout(function () { searchInput.focus(); }, 50);
|
|
@@ -7,6 +7,7 @@ import { toggleDarkMode, getCurrentTheme, getChatLayout, setChatLayout } from '.
|
|
|
7
7
|
import { showEmailSetupModal, getEmailAccountListCache } from './context-sources.js';
|
|
8
8
|
import { setSTTLang } from './stt.js';
|
|
9
9
|
import { userAvatarUrl } from './avatar.js';
|
|
10
|
+
import { store } from './store.js';
|
|
10
11
|
|
|
11
12
|
var ctx = null;
|
|
12
13
|
var settingsEl = null;
|
|
@@ -166,6 +167,26 @@ export function initUserSettings(appCtx) {
|
|
|
166
167
|
});
|
|
167
168
|
}
|
|
168
169
|
|
|
170
|
+
// Mates UI toggle. Default-on, so flipping off hides every Mates
|
|
171
|
+
// surface in the app (sidebar avatars, DM picker entry, home-hub strip)
|
|
172
|
+
// via the body.mates-disabled CSS gate. Flipping back on restores the
|
|
173
|
+
// surface; existing Mate data is never deleted.
|
|
174
|
+
var matesEnabledToggle = document.getElementById('us-mates-enabled');
|
|
175
|
+
if (matesEnabledToggle) {
|
|
176
|
+
matesEnabledToggle.addEventListener('change', function () {
|
|
177
|
+
var want = !!this.checked;
|
|
178
|
+
applyMatesEnabledClass(want);
|
|
179
|
+
store.set({ matesEnabled: want });
|
|
180
|
+
fetch('/api/user/mates-enabled', {
|
|
181
|
+
method: 'PUT',
|
|
182
|
+
headers: { 'Content-Type': 'application/json' },
|
|
183
|
+
body: JSON.stringify({ enabled: want }),
|
|
184
|
+
}).then(function (r) { return r.json(); }).then(function (data) {
|
|
185
|
+
if (data.ok) showToast(data.matesEnabled ? 'Mates on' : 'Mates off');
|
|
186
|
+
}).catch(function () {});
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
169
190
|
// Theme switcher (Light / Dark)
|
|
170
191
|
var themeSwitcher = document.getElementById('us-theme-switcher');
|
|
171
192
|
if (themeSwitcher) {
|
|
@@ -225,13 +246,16 @@ export function initUserSettings(appCtx) {
|
|
|
225
246
|
}
|
|
226
247
|
}
|
|
227
248
|
|
|
228
|
-
|
|
249
|
+
// Optional `initialSection` lets callers deep-link directly into a specific
|
|
250
|
+
// settings tab (e.g. the disabled-Mates picker button passing 'us-mates').
|
|
251
|
+
// Falls back to Account so existing callers and the icon click are unchanged.
|
|
252
|
+
export function openUserSettings(initialSection) {
|
|
229
253
|
settingsEl.classList.remove('hidden');
|
|
230
254
|
openBtn.classList.add('active');
|
|
231
255
|
refreshIcons(settingsEl);
|
|
232
256
|
populateAccount();
|
|
233
257
|
hidePinForm();
|
|
234
|
-
switchSection('us-account');
|
|
258
|
+
switchSection(initialSection || 'us-account');
|
|
235
259
|
}
|
|
236
260
|
|
|
237
261
|
export function closeUserSettings() {
|
|
@@ -259,6 +283,24 @@ function stopProp(e) {
|
|
|
259
283
|
e.stopPropagation();
|
|
260
284
|
}
|
|
261
285
|
|
|
286
|
+
// Mates UI gate. When the toggle is off we add `mates-disabled` to the
|
|
287
|
+
// body so CSS can hide every Mates surface (sidebar avatars, DM picker
|
|
288
|
+
// "Create a Mate" entry, home-hub mates strip) without touching the
|
|
289
|
+
// underlying data. JS code paths that gate on `store.get('matesEnabled')`
|
|
290
|
+
// also short-circuit so we don't, for example, open the mate wizard if
|
|
291
|
+
// someone fires it programmatically.
|
|
292
|
+
function applyMatesEnabledClass(enabled) {
|
|
293
|
+
if (!document.body) return;
|
|
294
|
+
document.body.classList.toggle('mates-disabled', !enabled);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function isMatesEnabled() {
|
|
298
|
+
// Default to true if the store hasn't received profile yet — populating
|
|
299
|
+
// the false state happens after /api/profile resolves, so an undefined
|
|
300
|
+
// value at boot should not hide Mates UI.
|
|
301
|
+
return store.get('matesEnabled') !== false;
|
|
302
|
+
}
|
|
303
|
+
|
|
262
304
|
function showPinForm() {
|
|
263
305
|
if (!pinForm) return;
|
|
264
306
|
pinForm.classList.remove('hidden');
|
|
@@ -392,6 +434,12 @@ function populateAccount() {
|
|
|
392
434
|
if (data.mateOnboardingShown) {
|
|
393
435
|
try { localStorage.setItem("clay-mate-onboarding-shown", "1"); } catch (e) {}
|
|
394
436
|
}
|
|
437
|
+
// Mates UI toggle: default true; treat any missing/non-false value as on
|
|
438
|
+
var matesOn = data.matesEnabled !== false;
|
|
439
|
+
var matesToggle = document.getElementById('us-mates-enabled');
|
|
440
|
+
if (matesToggle) matesToggle.checked = matesOn;
|
|
441
|
+
store.set({ matesEnabled: matesOn });
|
|
442
|
+
applyMatesEnabledClass(matesOn);
|
|
395
443
|
if (data.chatLayout) {
|
|
396
444
|
setChatLayout(data.chatLayout); // update local cache + CSS
|
|
397
445
|
}
|
package/lib/server-settings.js
CHANGED
|
@@ -29,6 +29,7 @@ function attachSettings(ctx) {
|
|
|
29
29
|
profile.autoContinueOnRateLimit = !!mu.autoContinueOnRateLimit;
|
|
30
30
|
profile.chatLayout = mu.chatLayout || "channel";
|
|
31
31
|
profile.mateOnboardingShown = !!mu.mateOnboardingShown;
|
|
32
|
+
profile.matesEnabled = mu.matesEnabled !== false;
|
|
32
33
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
33
34
|
res.end(JSON.stringify(profile));
|
|
34
35
|
return true;
|
|
@@ -50,6 +51,7 @@ function attachSettings(ctx) {
|
|
|
50
51
|
profile.autoContinueOnRateLimit = !!dc.autoContinueOnRateLimit;
|
|
51
52
|
profile.chatLayout = dc.chatLayout || "channel";
|
|
52
53
|
profile.mateOnboardingShown = !!dc.mateOnboardingShown;
|
|
54
|
+
profile.matesEnabled = dc.matesEnabled !== false;
|
|
53
55
|
}
|
|
54
56
|
// Check if custom avatar file exists
|
|
55
57
|
try {
|
|
@@ -401,6 +403,50 @@ function attachSettings(ctx) {
|
|
|
401
403
|
return true;
|
|
402
404
|
}
|
|
403
405
|
|
|
406
|
+
// PUT /api/user/mates-enabled
|
|
407
|
+
if (req.method === "PUT" && fullUrl === "/api/user/mates-enabled") {
|
|
408
|
+
var mu = getMultiUserFromReq(req);
|
|
409
|
+
if (!mu) {
|
|
410
|
+
// Single-user: store on daemon config
|
|
411
|
+
var body = "";
|
|
412
|
+
req.on("data", function (chunk) { body += chunk; });
|
|
413
|
+
req.on("end", function () {
|
|
414
|
+
try {
|
|
415
|
+
var data = JSON.parse(body);
|
|
416
|
+
var want = !!data.enabled;
|
|
417
|
+
if (typeof opts.onSetMatesEnabled === "function") {
|
|
418
|
+
opts.onSetMatesEnabled(want);
|
|
419
|
+
}
|
|
420
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
421
|
+
res.end(JSON.stringify({ ok: true, matesEnabled: want }));
|
|
422
|
+
} catch (e) {
|
|
423
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
424
|
+
res.end('{"error":"Invalid request"}');
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
var body = "";
|
|
430
|
+
req.on("data", function (chunk) { body += chunk; });
|
|
431
|
+
req.on("end", function () {
|
|
432
|
+
try {
|
|
433
|
+
var data = JSON.parse(body);
|
|
434
|
+
var result = users.setMatesEnabled(mu.id, !!data.enabled);
|
|
435
|
+
if (result.error) {
|
|
436
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
437
|
+
res.end(JSON.stringify({ error: result.error }));
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
441
|
+
res.end(JSON.stringify({ ok: true, matesEnabled: result.matesEnabled }));
|
|
442
|
+
} catch (e) {
|
|
443
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
444
|
+
res.end('{"error":"Invalid request"}');
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
|
|
404
450
|
// PUT /api/user/chat-layout
|
|
405
451
|
if (req.method === "PUT" && fullUrl === "/api/user/chat-layout") {
|
|
406
452
|
var mu = getMultiUserFromReq(req);
|
package/lib/users-preferences.js
CHANGED
|
@@ -217,6 +217,36 @@ function attachPreferences(deps) {
|
|
|
217
217
|
return { error: "User not found" };
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
// --- Per-user Mates UI toggle ---
|
|
221
|
+
//
|
|
222
|
+
// When false, the entire Mates surface (sidebar avatars, DM "Create a
|
|
223
|
+
// Mate" entry, home-hub mates strip) is hidden for this user. Default
|
|
224
|
+
// is ON: Mates is opt-out, not opt-in. Stored as `matesEnabled`; we
|
|
225
|
+
// treat any value other than the literal `false` as enabled so brand-
|
|
226
|
+
// new users (no field set) get the default-on experience.
|
|
227
|
+
|
|
228
|
+
function getMatesEnabled(userId) {
|
|
229
|
+
var data = loadUsers();
|
|
230
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
231
|
+
if (data.users[i].id === userId) {
|
|
232
|
+
return data.users[i].matesEnabled !== false;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function setMatesEnabled(userId, enabled) {
|
|
239
|
+
var data = loadUsers();
|
|
240
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
241
|
+
if (data.users[i].id === userId) {
|
|
242
|
+
data.users[i].matesEnabled = !!enabled;
|
|
243
|
+
saveUsers(data);
|
|
244
|
+
return { ok: true, matesEnabled: !!enabled };
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return { error: "User not found" };
|
|
248
|
+
}
|
|
249
|
+
|
|
220
250
|
// --- Per-user tool palette preferences ---
|
|
221
251
|
//
|
|
222
252
|
// Each user can customize the sidebar tool grid by reordering or
|
|
@@ -291,6 +321,8 @@ function attachPreferences(deps) {
|
|
|
291
321
|
setChatLayout: setChatLayout,
|
|
292
322
|
getAutoContinue: getAutoContinue,
|
|
293
323
|
setAutoContinue: setAutoContinue,
|
|
324
|
+
getMatesEnabled: getMatesEnabled,
|
|
325
|
+
setMatesEnabled: setMatesEnabled,
|
|
294
326
|
getToolPalettes: getToolPalettes,
|
|
295
327
|
setToolPalette: setToolPalette,
|
|
296
328
|
setMateOnboarded: setMateOnboarded,
|
package/lib/users.js
CHANGED
|
@@ -416,6 +416,8 @@ var getChatLayout = preferences.getChatLayout;
|
|
|
416
416
|
var setChatLayout = preferences.setChatLayout;
|
|
417
417
|
var getAutoContinue = preferences.getAutoContinue;
|
|
418
418
|
var setAutoContinue = preferences.setAutoContinue;
|
|
419
|
+
var getMatesEnabled = preferences.getMatesEnabled;
|
|
420
|
+
var setMatesEnabled = preferences.setMatesEnabled;
|
|
419
421
|
var getToolPalettes = preferences.getToolPalettes;
|
|
420
422
|
var setToolPalette = preferences.setToolPalette;
|
|
421
423
|
var setMateOnboarded = preferences.setMateOnboarded;
|
|
@@ -475,6 +477,8 @@ module.exports = {
|
|
|
475
477
|
setMateOnboarded: setMateOnboarded,
|
|
476
478
|
getAutoContinue: getAutoContinue,
|
|
477
479
|
setAutoContinue: setAutoContinue,
|
|
480
|
+
getMatesEnabled: getMatesEnabled,
|
|
481
|
+
setMatesEnabled: setMatesEnabled,
|
|
478
482
|
getToolPalettes: getToolPalettes,
|
|
479
483
|
setToolPalette: setToolPalette,
|
|
480
484
|
getDeletedBuiltinKeys: getDeletedBuiltinKeys,
|