clay-server 2.34.1-beta.2 → 2.34.1-beta.4
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.
|
@@ -103,30 +103,12 @@ function onConnected() {
|
|
|
103
103
|
|
|
104
104
|
// Session restore is now server-driven (user-presence.json).
|
|
105
105
|
// Mate DM restore is also server-driven via "restore_mate_dm" message.
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
console.log("[dm-restore] Server did not restore DM, using localStorage fallback:", savedDm);
|
|
113
|
-
openDm(savedDm);
|
|
114
|
-
}
|
|
115
|
-
}, 2000);
|
|
116
|
-
// Cancel fallback if server restores DM first
|
|
117
|
-
var patchedOnce = false;
|
|
118
|
-
var checkRestore = function (evt) {
|
|
119
|
-
try {
|
|
120
|
-
var d = JSON.parse(evt.data);
|
|
121
|
-
if (d.type === "restore_mate_dm" && !patchedOnce) {
|
|
122
|
-
patchedOnce = true;
|
|
123
|
-
clearTimeout(dmFallbackTimer);
|
|
124
|
-
}
|
|
125
|
-
} catch (e) {}
|
|
126
|
-
};
|
|
127
|
-
ws.addEventListener("message", checkRestore);
|
|
128
|
-
setTimeout(function () { ws.removeEventListener("message", checkRestore); }, 3000);
|
|
129
|
-
}
|
|
106
|
+
// Previously there was a 2s localStorage fallback that auto-called
|
|
107
|
+
// openDm(savedDm) on every reconnect. That fallback re-opened stale
|
|
108
|
+
// mate DMs on every refresh / project switch and was the root cause
|
|
109
|
+
// of the skill-install modal popping unprompted. Server-driven restore
|
|
110
|
+
// is authoritative — drop the client-side fallback entirely.
|
|
111
|
+
try { localStorage.removeItem("clay-active-dm"); } catch (e) {}
|
|
130
112
|
// Safety: clear returningFromMateDm after initial messages settle
|
|
131
113
|
if (store.get('returningFromMateDm')) {
|
|
132
114
|
setTimeout(function () {
|
|
@@ -21,7 +21,6 @@ import { closeTerminal } from './terminal.js';
|
|
|
21
21
|
import { openMobileSheet, setMobileSheetMateData } from './sidebar-mobile.js';
|
|
22
22
|
import { getProfileLang } from './profile.js';
|
|
23
23
|
import { isSchedulerOpen, closeScheduler } from './scheduler.js';
|
|
24
|
-
import { requireClayMateInterview } from './app-skills-install.js';
|
|
25
24
|
import { syncResizeHandles } from './sidebar.js';
|
|
26
25
|
|
|
27
26
|
var MATE_ONBOARDING_KEY = "clay-mate-onboarding-shown";
|
|
@@ -77,13 +76,15 @@ export function openDm(targetUserId) {
|
|
|
77
76
|
if (!ws || ws.readyState !== 1) return;
|
|
78
77
|
// Persist DM state for refresh recovery
|
|
79
78
|
try { localStorage.setItem("clay-active-dm", targetUserId); } catch (e) {}
|
|
80
|
-
//
|
|
79
|
+
// Opening an existing mate DM does not require the clay-mate-interview
|
|
80
|
+
// skill — that skill is only used during new mate creation / reshaping.
|
|
81
|
+
// Showing onboarding + gating a skill version check here caused the
|
|
82
|
+
// "Skill Installation Required" modal to pop on every refresh / project
|
|
83
|
+
// switch via the localStorage DM-restore fallback in app-connection.js.
|
|
81
84
|
if (typeof targetUserId === "string" && targetUserId.indexOf("mate_") === 0) {
|
|
82
85
|
showMateOnboarding(function () {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (ws2) ws2.send(JSON.stringify({ type: "dm_open", targetUserId: targetUserId }));
|
|
86
|
-
});
|
|
86
|
+
var ws2 = getWs();
|
|
87
|
+
if (ws2) ws2.send(JSON.stringify({ type: "dm_open", targetUserId: targetUserId }));
|
|
87
88
|
});
|
|
88
89
|
return;
|
|
89
90
|
}
|
|
@@ -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,25 +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 || [];
|
|
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.
|
|
200
255
|
var actionable = [];
|
|
201
256
|
for (var i = 0; i < results.length; i++) {
|
|
202
257
|
var r = results[i];
|
|
203
|
-
if (r.status
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
if (!orig) continue;
|
|
209
|
-
actionable.push({
|
|
210
|
-
name: r.name,
|
|
211
|
-
url: orig.url,
|
|
212
|
-
scope: orig.scope || "global",
|
|
213
|
-
installed: false,
|
|
214
|
-
status: r.status,
|
|
215
|
-
installedVersion: r.installedVersion,
|
|
216
|
-
remoteVersion: r.remoteVersion,
|
|
217
|
-
});
|
|
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; }
|
|
218
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
|
+
});
|
|
219
274
|
}
|
|
220
275
|
if (actionable.length === 0) { cb(); return; }
|
|
221
276
|
pendingSkillInstalls = actionable;
|