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
- // Fallback: if server doesn't restore DM within 2s, try localStorage
107
- var savedDm = null;
108
- try { savedDm = localStorage.getItem("clay-active-dm"); } catch (e) {}
109
- if (savedDm && !store.get('dmMode') && !store.get('mateProjectSlug')) {
110
- var dmFallbackTimer = setTimeout(function () {
111
- if (!store.get('dmMode') && savedDm) {
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
- // Check mate skill updates before opening mate DM
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
- requireClayMateInterview(function () {
84
- var ws2 = getWs();
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: false });
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", hideSkillInstallModal);
32
- skillInstallModal.querySelector(".confirm-backdrop").addEventListener("click", hideSkillInstallModal);
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 === "missing" || r.status === "outdated") {
204
- var orig = null;
205
- for (var j = 0; j < opts.skills.length; j++) {
206
- if (opts.skills[j].name === r.name) { orig = opts.skills[j]; break; }
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.34.1-beta.2",
3
+ "version": "2.34.1-beta.4",
4
4
  "description": "Self-hosted Claude Code in your browser. Multi-session, multi-user, push notifications.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",