clay-server 2.33.0 → 2.33.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.
@@ -31,15 +31,6 @@ var LANGUAGES = [
31
31
  { code: 'de-DE', name: 'German' },
32
32
  ];
33
33
 
34
- // --- Persist language choice ---
35
- function saveLang(code) {
36
- try { localStorage.setItem('stt-lang', code); } catch (e) { /* ignore */ }
37
- }
38
-
39
- function loadLang() {
40
- try { return localStorage.getItem('stt-lang'); } catch (e) { return null; }
41
- }
42
-
43
34
  // --- Check browser support ---
44
35
  function getSpeechRecognition() {
45
36
  return window.SpeechRecognition || window.webkitSpeechRecognition || null;
@@ -58,9 +49,6 @@ export function initSTT(_ctx) {
58
49
  return;
59
50
  }
60
51
 
61
- // Restore saved language
62
- selectedLang = loadLang();
63
-
64
52
  sttBtn.addEventListener('click', function(e) {
65
53
  e.stopPropagation();
66
54
 
@@ -135,7 +123,6 @@ function hideLangPopover() {
135
123
 
136
124
  function onLangSelected(code) {
137
125
  selectedLang = code;
138
- saveLang(code);
139
126
  hideLangPopover();
140
127
  startRecording();
141
128
  }
@@ -263,7 +250,6 @@ function stopRecording() {
263
250
  // --- External lang setter (used by profile module) ---
264
251
  export function setSTTLang(code) {
265
252
  selectedLang = code;
266
- saveLang(code);
267
253
  }
268
254
 
269
255
  export function getSTTLang() {
@@ -1,3 +1,4 @@
1
+ import { iconHtml, refreshIcons } from './icons.js';
1
2
  import { setTerminalTheme } from './terminal.js';
2
3
  import { updateMermaidTheme } from './markdown.js';
3
4
 
@@ -537,6 +538,16 @@ function updateToggleIcon() {
537
538
  // User settings toggle
538
539
  var usToggle = document.getElementById("us-theme-toggle");
539
540
  if (usToggle) usToggle.checked = isLight;
541
+ // User island toggle
542
+ var islandToggle = document.getElementById("user-theme-toggle-btn");
543
+ if (islandToggle) {
544
+ islandToggle.classList.remove("theme-toggle-light", "theme-toggle-dark");
545
+ islandToggle.classList.add(isLight ? "theme-toggle-light" : "theme-toggle-dark");
546
+ islandToggle.innerHTML = iconHtml(isLight ? "moon" : "sun");
547
+ islandToggle.title = isLight ? "Switch to dark mode" : "Switch to light mode";
548
+ islandToggle.setAttribute("aria-label", islandToggle.title);
549
+ }
550
+ refreshIcons();
540
551
  }
541
552
 
542
553
  // --- Theme picker UI ---
@@ -734,6 +745,14 @@ export function initTheme() {
734
745
 
735
746
  // Set initial toggle icon
736
747
  updateToggleIcon();
748
+
749
+ var islandToggle = document.getElementById("user-theme-toggle-btn");
750
+ if (islandToggle && !islandToggle._clayThemeBound) {
751
+ islandToggle._clayThemeBound = true;
752
+ islandToggle.addEventListener("click", function () {
753
+ toggleDarkMode();
754
+ });
755
+ }
737
756
  }
738
757
 
739
758
  // --- Settings picker (for appearance section in server settings) ---
@@ -5,6 +5,8 @@ import { refreshIcons } from './icons.js';
5
5
  import { showToast } from './utils.js';
6
6
  import { toggleDarkMode, getCurrentTheme, getChatLayout, setChatLayout } from './theme.js';
7
7
  import { showEmailSetupModal, getEmailAccountListCache } from './context-sources.js';
8
+ import { setSTTLang } from './stt.js';
9
+ import { userAvatarUrl } from './avatar.js';
8
10
 
9
11
  var ctx = null;
10
12
  var settingsEl = null;
@@ -13,6 +15,11 @@ var closeBtn = null;
13
15
  var backdrop = null;
14
16
  var navItems = null;
15
17
  var sections = null;
18
+ var pinForm = null;
19
+ var pinSetBtn = null;
20
+ var pinCancelBtn = null;
21
+ var pinEnabled = false;
22
+ var currentProfile = null;
16
23
 
17
24
 
18
25
  export function initUserSettings(appCtx) {
@@ -26,6 +33,7 @@ export function initUserSettings(appCtx) {
26
33
 
27
34
  navItems = settingsEl.querySelectorAll('.us-nav-item');
28
35
  sections = settingsEl.querySelectorAll('.us-section');
36
+ pinForm = document.getElementById('us-pin-form');
29
37
 
30
38
  openBtn.addEventListener('click', function () {
31
39
  openUserSettings();
@@ -66,19 +74,83 @@ export function initUserSettings(appCtx) {
66
74
 
67
75
  // PIN save button
68
76
  var pinInput = document.getElementById('us-pin-input');
77
+ var pinConfirmInput = document.getElementById('us-pin-confirm-input');
78
+ var pinCurrentInput = document.getElementById('us-pin-current-input');
69
79
  var pinSave = document.getElementById('us-pin-save');
70
- if (pinInput && pinSave) {
80
+ pinSetBtn = document.getElementById('us-pin-set-btn');
81
+ pinCancelBtn = document.getElementById('us-pin-cancel');
82
+ if (pinInput && pinConfirmInput && pinSave) {
71
83
  function validatePin() {
72
- pinSave.disabled = !/^\d{6}$/.test(pinInput.value);
84
+ var validNew = /^\d{6}$/.test(pinInput.value);
85
+ var validConfirm = /^\d{6}$/.test(pinConfirmInput.value);
86
+ var validCurrent = !pinEnabled || (pinCurrentInput && /^\d{6}$/.test(pinCurrentInput.value));
87
+ pinSave.disabled = !(validNew && validConfirm && validCurrent && pinInput.value === pinConfirmInput.value);
73
88
  }
74
89
  pinInput.addEventListener('input', validatePin);
90
+ pinConfirmInput.addEventListener('input', validatePin);
91
+ if (pinCurrentInput) pinCurrentInput.addEventListener('input', validatePin);
75
92
  pinInput.addEventListener('keyup', function (e) { e.stopPropagation(); validatePin(); });
76
- pinInput.addEventListener('keydown', stopProp);
93
+ pinConfirmInput.addEventListener('keyup', function (e) { e.stopPropagation(); validatePin(); });
94
+ if (pinCurrentInput) pinCurrentInput.addEventListener('keyup', function (e) { e.stopPropagation(); validatePin(); });
95
+ pinInput.addEventListener('keydown', function (e) {
96
+ e.stopPropagation();
97
+ if (e.key === 'Enter') {
98
+ e.preventDefault();
99
+ savePin(pinInput.value);
100
+ }
101
+ if (e.key === 'Escape') {
102
+ e.preventDefault();
103
+ hidePinForm();
104
+ }
105
+ });
106
+ pinConfirmInput.addEventListener('keydown', function (e) {
107
+ e.stopPropagation();
108
+ if (e.key === 'Enter') {
109
+ e.preventDefault();
110
+ savePin(pinInput.value);
111
+ }
112
+ if (e.key === 'Escape') {
113
+ e.preventDefault();
114
+ hidePinForm();
115
+ }
116
+ });
117
+ if (pinCurrentInput) {
118
+ pinCurrentInput.addEventListener('keydown', function (e) {
119
+ e.stopPropagation();
120
+ if (e.key === 'Enter') {
121
+ e.preventDefault();
122
+ savePin(pinInput.value);
123
+ }
124
+ if (e.key === 'Escape') {
125
+ e.preventDefault();
126
+ hidePinForm();
127
+ }
128
+ });
129
+ }
77
130
  pinInput.addEventListener('keypress', stopProp);
131
+ pinConfirmInput.addEventListener('keypress', stopProp);
132
+ if (pinCurrentInput) pinCurrentInput.addEventListener('keypress', stopProp);
78
133
  pinSave.addEventListener('click', function () {
79
134
  savePin(pinInput.value);
80
135
  });
81
136
  }
137
+ if (pinSetBtn) {
138
+ pinSetBtn.addEventListener('click', function () {
139
+ showPinForm();
140
+ });
141
+ }
142
+ if (pinCancelBtn) {
143
+ pinCancelBtn.addEventListener('click', function () {
144
+ hidePinForm();
145
+ });
146
+ }
147
+
148
+ var langSelect = document.getElementById('us-lang-select');
149
+ if (langSelect) {
150
+ langSelect.addEventListener('change', function () {
151
+ saveProfileChange({ lang: this.value });
152
+ });
153
+ }
82
154
 
83
155
  // Auto-continue toggle
84
156
  var autoContinueToggle = document.getElementById('us-auto-continue');
@@ -158,6 +230,7 @@ function openUserSettings() {
158
230
  openBtn.classList.add('active');
159
231
  refreshIcons(settingsEl);
160
232
  populateAccount();
233
+ hidePinForm();
161
234
  switchSection('us-account');
162
235
  }
163
236
 
@@ -186,6 +259,63 @@ function stopProp(e) {
186
259
  e.stopPropagation();
187
260
  }
188
261
 
262
+ function showPinForm() {
263
+ if (!pinForm) return;
264
+ pinForm.classList.remove('hidden');
265
+ var pinCurrentRow = document.getElementById('us-pin-current-row');
266
+ var pinInput = document.getElementById('us-pin-input');
267
+ var pinConfirmInput = document.getElementById('us-pin-confirm-input');
268
+ var pinCurrentInput = document.getElementById('us-pin-current-input');
269
+ var pinSave = document.getElementById('us-pin-save');
270
+ var pinMsg = document.getElementById('us-pin-msg');
271
+ var pinFormTitle = document.getElementById('us-pin-form-title');
272
+ if (pinFormTitle) {
273
+ pinFormTitle.textContent = pinEnabled ? 'Change PIN' : 'Set PIN';
274
+ }
275
+ if (pinCurrentRow) {
276
+ pinCurrentRow.classList.toggle('hidden', !pinEnabled);
277
+ }
278
+ if (pinInput) {
279
+ pinInput.value = '';
280
+ }
281
+ if (pinConfirmInput) pinConfirmInput.value = '';
282
+ if (pinCurrentInput) pinCurrentInput.value = '';
283
+ if (pinSave) pinSave.disabled = true;
284
+ if (pinSave) pinSave.textContent = 'Save';
285
+ if (pinMsg) {
286
+ pinMsg.textContent = '';
287
+ pinMsg.className = 'settings-hint settings-pin-error hidden';
288
+ }
289
+ if (pinEnabled && pinCurrentInput) {
290
+ pinCurrentInput.focus();
291
+ } else if (pinInput) {
292
+ pinInput.focus();
293
+ }
294
+ }
295
+
296
+ function hidePinForm() {
297
+ if (!pinForm) return;
298
+ pinForm.classList.add('hidden');
299
+ var pinCurrentRow = document.getElementById('us-pin-current-row');
300
+ var pinInput = document.getElementById('us-pin-input');
301
+ var pinConfirmInput = document.getElementById('us-pin-confirm-input');
302
+ var pinCurrentInput = document.getElementById('us-pin-current-input');
303
+ var pinSave = document.getElementById('us-pin-save');
304
+ var pinMsg = document.getElementById('us-pin-msg');
305
+ if (pinCurrentRow) {
306
+ pinCurrentRow.classList.remove('hidden');
307
+ }
308
+ if (pinInput) pinInput.value = '';
309
+ if (pinConfirmInput) pinConfirmInput.value = '';
310
+ if (pinCurrentInput) pinCurrentInput.value = '';
311
+ if (pinSave) pinSave.disabled = true;
312
+ if (pinSave) pinSave.textContent = 'Save';
313
+ if (pinMsg) {
314
+ pinMsg.textContent = '';
315
+ pinMsg.className = 'settings-hint settings-pin-error hidden';
316
+ }
317
+ }
318
+
189
319
  // --- Account population ---
190
320
 
191
321
  function populateAccount() {
@@ -194,15 +324,57 @@ function populateAccount() {
194
324
  return r.json();
195
325
  }).then(function (data) {
196
326
  if (!data) return;
197
- var usernameEl = document.getElementById('us-username');
198
- if (usernameEl && data.username) {
199
- usernameEl.textContent = data.username;
327
+ currentProfile = data;
328
+ var displayName = data.name || data.displayName || data.username || 'Clay User';
329
+ var avatarImg = document.getElementById('us-account-avatar-img');
330
+ var avatarFallback = document.getElementById('us-account-avatar-fallback');
331
+ var accountName = document.getElementById('us-account-name');
332
+ var accountSubline = document.getElementById('us-account-subline');
333
+ var langSelect = document.getElementById('us-lang-select');
334
+ var pinSetBtnEl = document.getElementById('us-pin-set-btn');
335
+ var pinFormTitle = document.getElementById('us-pin-form-title');
336
+ if (accountName) {
337
+ accountName.textContent = displayName;
338
+ }
339
+ if (accountSubline) {
340
+ var parts = [];
341
+ if (data.username) parts.push('@' + data.username);
342
+ if (data.role) parts.push(data.role.charAt(0).toUpperCase() + data.role.slice(1));
343
+ accountSubline.textContent = parts.length ? parts.join(' · ') : 'Local profile';
344
+ }
345
+ pinEnabled = !!data.pinEnabled;
346
+ if (pinSetBtnEl) {
347
+ pinSetBtnEl.textContent = pinEnabled ? 'Change PIN' : 'Set PIN';
348
+ }
349
+ if (pinFormTitle) {
350
+ pinFormTitle.textContent = pinEnabled ? 'Change PIN' : 'Set PIN';
351
+ }
352
+ if (langSelect) {
353
+ langSelect.value = data.lang || 'en-US';
354
+ }
355
+ if (data.lang) {
356
+ setSTTLang(data.lang);
357
+ }
358
+ if (avatarImg && avatarFallback) {
359
+ var avatarUrl = getCurrentUserAvatarSrc(data);
360
+ avatarImg.alt = displayName;
361
+ avatarImg.classList.remove('hidden');
362
+ avatarFallback.classList.remove('hidden');
363
+ avatarFallback.textContent = avatarInitials(displayName);
364
+ avatarImg.onload = function () {
365
+ avatarFallback.classList.add('hidden');
366
+ };
367
+ avatarImg.onerror = function () {
368
+ avatarFallback.classList.remove('hidden');
369
+ };
370
+ avatarImg.src = avatarUrl;
371
+ if (avatarImg.complete && avatarImg.naturalWidth > 0) {
372
+ avatarFallback.classList.add('hidden');
373
+ }
200
374
  }
201
375
  // Hide account section in single-user mode (no username)
202
376
  var accountNav = settingsEl.querySelector('[data-section="us-account"]');
203
- if (accountNav && !data.username) {
204
- accountNav.style.display = 'none';
205
- }
377
+ if (accountNav) accountNav.style.display = data.username ? '' : 'none';
206
378
  // Auto-continue toggle
207
379
  var acToggle = document.getElementById('us-auto-continue');
208
380
  if (acToggle) acToggle.checked = !!data.autoContinueOnRateLimit;
@@ -234,10 +406,89 @@ function populateAccount() {
234
406
  }).catch(function () {});
235
407
  }
236
408
 
409
+ function saveProfileChange(updates) {
410
+ if (!currentProfile) return;
411
+ var nextProfile = {};
412
+ var payload = {};
413
+ var key;
414
+ for (key in currentProfile) {
415
+ if (Object.prototype.hasOwnProperty.call(currentProfile, key)) {
416
+ nextProfile[key] = currentProfile[key];
417
+ }
418
+ }
419
+ for (key in updates) {
420
+ if (Object.prototype.hasOwnProperty.call(updates, key)) {
421
+ nextProfile[key] = updates[key];
422
+ }
423
+ }
424
+ currentProfile = nextProfile;
425
+ if (updates.lang) {
426
+ setSTTLang(updates.lang);
427
+ }
428
+ payload.name = nextProfile.name;
429
+ payload.lang = nextProfile.lang;
430
+ payload.avatarColor = nextProfile.avatarColor;
431
+ payload.avatarStyle = nextProfile.avatarStyle;
432
+ payload.avatarSeed = nextProfile.avatarSeed;
433
+ payload.avatarCustom = nextProfile.avatarCustom;
434
+ fetch('/api/profile', {
435
+ method: 'PUT',
436
+ headers: { 'Content-Type': 'application/json' },
437
+ body: JSON.stringify(payload),
438
+ }).catch(function () {});
439
+ }
440
+
441
+ function avatarInitials(value) {
442
+ var text = (value || '').trim();
443
+ if (!text) return '?';
444
+ var parts = text.split(/\s+/);
445
+ var first = parts[0].charAt(0);
446
+ var second = parts.length > 1 ? parts[1].charAt(0) : parts[0].charAt(1);
447
+ var out = first + (second || '');
448
+ return out.toUpperCase();
449
+ }
450
+
451
+ function getCurrentUserAvatarSrc(data) {
452
+ var islandAvatar = document.querySelector('.user-island-avatar img');
453
+ if (islandAvatar && islandAvatar.src) return islandAvatar.src;
454
+ if (data && data.avatarCustom) return data.avatarCustom;
455
+ return userAvatarUrl(data, 96);
456
+ }
457
+
237
458
  function savePin(pin) {
238
459
  var pinInput = document.getElementById('us-pin-input');
460
+ var pinConfirmInput = document.getElementById('us-pin-confirm-input');
461
+ var pinCurrentInput = document.getElementById('us-pin-current-input');
239
462
  var pinSave = document.getElementById('us-pin-save');
240
463
  var pinMsg = document.getElementById('us-pin-msg');
464
+ var currentPin = pinCurrentInput ? pinCurrentInput.value.trim() : '';
465
+ var confirmPin = pinConfirmInput ? pinConfirmInput.value.trim() : '';
466
+ var newPin = pinInput ? pinInput.value.trim() : '';
467
+
468
+ if (!/^\d{6}$/.test(newPin)) {
469
+ if (pinMsg) {
470
+ pinMsg.textContent = 'PIN must be exactly 6 digits.';
471
+ pinMsg.className = 'settings-hint settings-pin-error us-pin-msg-err';
472
+ pinMsg.classList.remove('hidden');
473
+ }
474
+ return;
475
+ }
476
+ if (confirmPin !== newPin) {
477
+ if (pinMsg) {
478
+ pinMsg.textContent = 'PINs do not match.';
479
+ pinMsg.className = 'settings-hint settings-pin-error us-pin-msg-err';
480
+ pinMsg.classList.remove('hidden');
481
+ }
482
+ return;
483
+ }
484
+ if (pinEnabled && !/^\d{6}$/.test(currentPin)) {
485
+ if (pinMsg) {
486
+ pinMsg.textContent = 'Current PIN is required.';
487
+ pinMsg.className = 'settings-hint settings-pin-error us-pin-msg-err';
488
+ pinMsg.classList.remove('hidden');
489
+ }
490
+ return;
491
+ }
241
492
 
242
493
  pinSave.disabled = true;
243
494
  pinSave.textContent = 'Saving\u2026';
@@ -245,32 +496,26 @@ function savePin(pin) {
245
496
  fetch('/api/user/pin', {
246
497
  method: 'PUT',
247
498
  headers: { 'Content-Type': 'application/json' },
248
- body: JSON.stringify({ newPin: pin }),
499
+ body: JSON.stringify({ currentPin: currentPin, newPin: newPin }),
249
500
  }).then(function (r) { return r.json(); }).then(function (data) {
250
501
  if (data.ok) {
251
- pinInput.value = '';
252
- pinSave.textContent = 'Change PIN';
253
- if (pinMsg) {
254
- pinMsg.textContent = 'Your PIN has been changed.';
255
- pinMsg.className = 'us-pin-msg us-pin-msg-ok';
256
- pinMsg.classList.remove('hidden');
257
- }
258
502
  showToast('PIN changed');
503
+ hidePinForm();
259
504
  } else {
260
505
  pinSave.disabled = false;
261
- pinSave.textContent = 'Change PIN';
506
+ pinSave.textContent = 'Save';
262
507
  if (pinMsg) {
263
508
  pinMsg.textContent = data.error || 'Could not change your PIN. Please try again.';
264
- pinMsg.className = 'us-pin-msg us-pin-msg-err';
509
+ pinMsg.className = 'settings-hint settings-pin-error us-pin-msg-err';
265
510
  pinMsg.classList.remove('hidden');
266
511
  }
267
512
  }
268
513
  }).catch(function () {
269
514
  pinSave.disabled = false;
270
- pinSave.textContent = 'Change PIN';
515
+ pinSave.textContent = 'Save';
271
516
  if (pinMsg) {
272
517
  pinMsg.textContent = 'Connection lost. Check your network and try again.';
273
- pinMsg.className = 'us-pin-msg us-pin-msg-err';
518
+ pinMsg.className = 'settings-hint settings-pin-error us-pin-msg-err';
274
519
  pinMsg.classList.remove('hidden');
275
520
  }
276
521
  });
package/lib/sdk-bridge.js CHANGED
@@ -120,6 +120,61 @@ function createSDKBridge(opts) {
120
120
  var clayTls = opts.clayTls || false;
121
121
  var clayAuthToken = opts.clayAuthToken || null;
122
122
  var onProcessingChanged = opts.onProcessingChanged || function () {};
123
+ var _cachedFreshAuthState = null;
124
+ var _cachedFreshAuthAt = 0;
125
+
126
+ function getFreshAuthState(force) {
127
+ var yoke = require("./yoke");
128
+ var now = Date.now();
129
+ if (!force && _cachedFreshAuthState && now - _cachedFreshAuthAt < 15000) {
130
+ return _cachedFreshAuthState;
131
+ }
132
+ if (force) yoke.invalidateAuthCache();
133
+ _cachedFreshAuthState = yoke.checkAuth();
134
+ _cachedFreshAuthAt = now;
135
+ return _cachedFreshAuthState;
136
+ }
137
+
138
+ function isAuthErrorMessage(errDetail) {
139
+ if (!errDetail) return false;
140
+ var errLower = String(errDetail).toLowerCase();
141
+ return errLower.indexOf("not logged in") !== -1
142
+ || errLower.indexOf("unauthenticated") !== -1
143
+ || errLower.indexOf("authentication") !== -1
144
+ || errLower.indexOf("sign in") !== -1
145
+ || errLower.indexOf("log in") !== -1
146
+ || errLower.indexOf("please login") !== -1;
147
+ }
148
+
149
+ function getLoginCommand(vendor) {
150
+ if (vendor === "codex") return "codex login --device-auth";
151
+ if (vendor === "claude") return "claude login";
152
+ return (vendor || "claude") + " login";
153
+ }
154
+
155
+ function notifyAuthRequired(session, title, body, authLinuxUser, canAutoLogin, loginCommand) {
156
+ var _nm = getNotificationsModule();
157
+ if (!_nm) return false;
158
+ _nm.notify("auth_required", {
159
+ title: title,
160
+ body: body,
161
+ slug: slug,
162
+ sessionId: session.localId,
163
+ ownerId: session.ownerId || null,
164
+ vendor: session.vendor || (adapter && adapter.vendor) || "claude",
165
+ loginCommand: loginCommand,
166
+ linuxUser: authLinuxUser,
167
+ canAutoLogin: canAutoLogin,
168
+ });
169
+ return true;
170
+ }
171
+
172
+ function logAuthDecision(stage, session, errDetail, authState) {
173
+ var vendor = session && session.vendor ? session.vendor : "(none)";
174
+ var errSnippet = errDetail ? String(errDetail).replace(/\s+/g, " ").slice(0, 180) : "";
175
+ var authSummary = authState ? JSON.stringify(authState) : "(none)";
176
+ console.warn("[sdk-bridge] auth decision [" + stage + "] vendor=" + vendor + " auth=" + authSummary + (errSnippet ? " err=" + errSnippet : ""));
177
+ }
123
178
 
124
179
  function getModelsForVendor(vendor) {
125
180
  if (vendor && sm.modelsByVendor && sm.modelsByVendor[vendor]) return sm.modelsByVendor[vendor];
@@ -737,29 +792,49 @@ function createSDKBridge(opts) {
737
792
  var isContextOverflow = errLower.indexOf("prompt is too long") !== -1
738
793
  || errLower.indexOf("context_length") !== -1
739
794
  || errLower.indexOf("maximum context length") !== -1;
740
- var isAuthError = errLower.indexOf("not logged in") !== -1
741
- || errLower.indexOf("unauthenticated") !== -1
742
- || errLower.indexOf("authentication") !== -1
743
- || errLower.indexOf("sign in") !== -1
744
- || errLower.indexOf("log in") !== -1
745
- || errLower.indexOf("please login") !== -1;
795
+ var isAuthError = isAuthErrorMessage(errDetail);
746
796
  if (isContextOverflow) {
747
797
  sendAndRecord(session, {
748
798
  type: "context_overflow",
749
799
  text: "Conversation too long to continue.",
750
800
  });
751
801
  } else if (isAuthError) {
802
+ var freshAuth = getFreshAuthState();
803
+ logAuthDecision("catch-auth-error", session, errDetail, freshAuth);
804
+ if (freshAuth[session.vendor]) {
805
+ sendAndRecord(session, {
806
+ type: "error",
807
+ text: "Authentication looked fine, but " + (session.vendor || "the vendor") + " returned an auth-like error.",
808
+ });
809
+ sendAndRecord(session, { type: "done", code: 1 });
810
+ sm.broadcastSessionList();
811
+ return;
812
+ }
752
813
  var authUser = session.ownerId ? usersModule.findUserById(session.ownerId) : null;
753
814
  var authLinuxUser = authUser && authUser.linuxUser ? authUser.linuxUser : null;
754
815
  var canAutoLogin = !usersModule.isMultiUser()
755
816
  || !!authLinuxUser
756
817
  || (authUser && authUser.role === "admin");
757
- sendAndRecord(session, {
818
+ var authTitle = (session.vendor === "codex" ? "Codex" : "Claude Code") + " is not logged in.";
819
+ var authMsg = {
758
820
  type: "auth_required",
759
- text: "Claude Code is not logged in.",
821
+ text: authTitle,
822
+ vendor: session.vendor || (adapter && adapter.vendor) || "claude",
823
+ loginCommand: getLoginCommand(session.vendor || (adapter && adapter.vendor) || "claude"),
760
824
  linuxUser: authLinuxUser,
761
825
  canAutoLogin: canAutoLogin,
762
- });
826
+ };
827
+ sendAndRecord(session, authMsg);
828
+ if (!notifyAuthRequired(
829
+ session,
830
+ authTitle,
831
+ "Open a terminal, then click the URL and follow the instructions.",
832
+ authLinuxUser,
833
+ canAutoLogin,
834
+ getLoginCommand(session.vendor || (adapter && adapter.vendor) || "claude")
835
+ )) {
836
+ // chat message already sent above
837
+ }
763
838
  } else {
764
839
  sendAndRecord(session, { type: "error", text: "Claude process error: " + err.message });
765
840
  }
@@ -968,6 +1043,16 @@ function createSDKBridge(opts) {
968
1043
  } else if (session.vendor) {
969
1044
  await ensureVendorReady(session.vendor);
970
1045
  }
1046
+ if (session.vendor && !adapters[session.vendor]) {
1047
+ var freshAuth = getFreshAuthState();
1048
+ logAuthDecision("pre-auth-required", session, null, freshAuth);
1049
+ if (freshAuth[session.vendor]) {
1050
+ var recoveredAdapter = await ensureVendorReady(session.vendor);
1051
+ if (recoveredAdapter) {
1052
+ console.log("[sdk-bridge] Auth recheck recovered adapter for " + session.vendor);
1053
+ }
1054
+ }
1055
+ }
971
1056
  // If still not available after lazy check, send auth_required
972
1057
  if (session.vendor && !adapters[session.vendor]) {
973
1058
  var vendorName = session.vendor.charAt(0).toUpperCase() + session.vendor.slice(1);
@@ -976,14 +1061,35 @@ function createSDKBridge(opts) {
976
1061
  var canAutoLogin = !usersModule.isMultiUser()
977
1062
  || !!authLinuxUser
978
1063
  || (authUser && authUser.role === "admin");
979
- sendAndRecord(session, {
1064
+ var authState = getFreshAuthState();
1065
+ logAuthDecision("emit-auth-required", session, "missing adapter", authState);
1066
+ if (authState[session.vendor]) {
1067
+ sendAndRecord(session, {
1068
+ type: "error",
1069
+ text: vendorName + " auth is available, but the adapter could not be initialized.",
1070
+ });
1071
+ sendAndRecord(session, { type: "done", code: 1 });
1072
+ return;
1073
+ }
1074
+ var authMsg2 = {
980
1075
  type: "auth_required",
981
1076
  text: vendorName + " is not logged in.",
982
1077
  vendor: session.vendor,
983
- loginCommand: session.vendor === "codex" ? "codex login --device-auth" : session.vendor + " login",
1078
+ loginCommand: getLoginCommand(session.vendor),
984
1079
  linuxUser: authLinuxUser,
985
1080
  canAutoLogin: canAutoLogin,
986
- });
1081
+ };
1082
+ sendAndRecord(session, authMsg2);
1083
+ if (!notifyAuthRequired(
1084
+ session,
1085
+ vendorName + " is not logged in.",
1086
+ "Open a terminal, then click the URL and follow the instructions.",
1087
+ authLinuxUser,
1088
+ canAutoLogin,
1089
+ getLoginCommand(session.vendor)
1090
+ )) {
1091
+ // chat message already sent above
1092
+ }
987
1093
  sendAndRecord(session, { type: "done", code: 1 });
988
1094
  return;
989
1095
  }
@@ -18,7 +18,7 @@ function attachMessageProcessor(ctx) {
18
18
  var discoverSkillDirs = ctx.discoverSkillDirs;
19
19
  var mergeSkills = ctx.mergeSkills;
20
20
 
21
- var AUTO_TITLE_TURN_THRESHOLD = 3;
21
+ var AUTO_TITLE_TURN_THRESHOLD = 2;
22
22
 
23
23
  function getMateIdForNotification() {
24
24
  if (!isMate) return null;
@@ -435,12 +435,33 @@ function attachMessageProcessor(ctx) {
435
435
  var canAutoLogin = !usersModule.isMultiUser()
436
436
  || !!authLinuxUser
437
437
  || (authUser && authUser.role === "admin");
438
- sendAndRecord(session, {
438
+ var authTitle = session.vendor === "codex" ? "Codex is not logged in." : "Claude Code is not logged in.";
439
+ var loginCommand = session.vendor === "codex"
440
+ ? "codex login --device-auth"
441
+ : "claude login";
442
+ var _nmLogin = getNotificationsModule();
443
+ var authMsg = {
439
444
  type: "auth_required",
440
- text: "Claude Code is not logged in.",
445
+ text: authTitle,
446
+ vendor: session.vendor || "claude",
447
+ loginCommand: loginCommand,
441
448
  linuxUser: authLinuxUser,
442
449
  canAutoLogin: canAutoLogin,
443
- });
450
+ };
451
+ sendAndRecord(session, authMsg);
452
+ if (_nmLogin) {
453
+ _nmLogin.notify("auth_required", {
454
+ title: authTitle,
455
+ body: "Open a terminal, then click the URL and follow the instructions.",
456
+ slug: slug,
457
+ sessionId: session.localId,
458
+ ownerId: session.ownerId || null,
459
+ vendor: session.vendor || "claude",
460
+ loginCommand: loginCommand,
461
+ linuxUser: authLinuxUser,
462
+ canAutoLogin: canAutoLogin,
463
+ });
464
+ }
444
465
  // Reset CLI session so next query starts fresh with new auth
445
466
  session.cliSessionId = null;
446
467
  }