clay-server 2.45.0-beta.2 → 2.45.0-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.
- package/lib/project-user-message.js +2 -1
- package/lib/project.js +14 -7
- package/lib/public/css/tui-attention.css +6 -0
- package/lib/public/modules/app-messages.js +30 -4
- package/lib/public/modules/app-notifications.js +58 -1
- package/lib/public/modules/tui-attention.js +6 -0
- package/lib/sdk-message-processor.js +53 -34
- package/lib/whats-new-content.js +16 -0
- package/lib/whats-new.js +3 -0
- package/lib/yoke/adapters/codex.js +57 -17
- package/lib/yoke/codex-app-server.js +30 -1
- package/package.json +2 -2
|
@@ -145,7 +145,8 @@ function attachUserMessage(ctx) {
|
|
|
145
145
|
return true;
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
|
-
var t = tm.create(msg.cols || 80, msg.rows || 24, getOsUserInfoForWs(ws), ws
|
|
148
|
+
var t = tm.create(msg.cols || 80, msg.rows || 24, getOsUserInfoForWs(ws), ws,
|
|
149
|
+
msg.initialCommand ? { initialInput: String(msg.initialCommand) } : undefined);
|
|
149
150
|
if (!t) {
|
|
150
151
|
sendTo(ws, { type: "term_error", error: "Cannot create terminal (node-pty not available or limit reached)" });
|
|
151
152
|
return true;
|
package/lib/project.js
CHANGED
|
@@ -907,13 +907,20 @@ function createProjectContext(opts) {
|
|
|
907
907
|
slug: slug,
|
|
908
908
|
});
|
|
909
909
|
} else if ((!sm.modelsByVendor || !sm.modelsByVendor[msg.vendor]) && typeof vendorAdapter.init === "function") {
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
910
|
+
// Init warms the adapter, but a slow/failed init must not block
|
|
911
|
+
// model listing (e.g. Codex models are a fixed list). Keep going
|
|
912
|
+
// to supportedModels() even if init throws.
|
|
913
|
+
try {
|
|
914
|
+
await vendorAdapter.init({
|
|
915
|
+
cwd: cwd,
|
|
916
|
+
clayPort: serverPort,
|
|
917
|
+
clayTls: serverTls,
|
|
918
|
+
clayAuthToken: serverAuthToken,
|
|
919
|
+
slug: slug,
|
|
920
|
+
});
|
|
921
|
+
} catch (e) {
|
|
922
|
+
console.error("[project] " + msg.vendor + " init failed (continuing to model list):", e.message || e);
|
|
923
|
+
}
|
|
917
924
|
}
|
|
918
925
|
if (vendorAdapter) {
|
|
919
926
|
sm.availableVendors = Object.keys(adapters);
|
|
@@ -349,6 +349,12 @@ body.tui-suspended #tui-resume-bar {
|
|
|
349
349
|
border: 1px solid var(--border);
|
|
350
350
|
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.6);
|
|
351
351
|
}
|
|
352
|
+
/* Compact variant for short content (e.g. login wizard): smaller box so the
|
|
353
|
+
terminal hugs the content instead of leaving a tall empty area below. */
|
|
354
|
+
.tui-modal.tui-modal-compact {
|
|
355
|
+
width: min(720px, 96%);
|
|
356
|
+
height: min(480px, 90%);
|
|
357
|
+
}
|
|
352
358
|
.tui-modal-header {
|
|
353
359
|
display: flex;
|
|
354
360
|
align-items: center;
|
|
@@ -29,7 +29,7 @@ import { updateSettingsModels, updateSettingsStats, updateDaemonConfig, handleSe
|
|
|
29
29
|
import { handleTermList, handleTermCreated, sendTerminalCommand, handleTermOutput, handleTermResized, handleTermExited, handleTermClosed } from './terminal.js';
|
|
30
30
|
import { attachTuiView, detachTuiView, setTuiSuspendedView, tuiHandleTermOutput, tuiHandleTermResized, tuiHandleTermExited, tuiHandleTermClosed } from './session-tui-view.js';
|
|
31
31
|
import { handleTuiTranscriptState } from './tui-grab.js';
|
|
32
|
-
import { tuiModalHandleTermOutput, tuiModalHandleTermResized, tuiModalHandleTermExited, tuiModalHandleTermClosed } from './tui-attention.js';
|
|
32
|
+
import { tuiModalHandleTermOutput, tuiModalHandleTermResized, tuiModalHandleTermExited, tuiModalHandleTermClosed, openTuiModal } from './tui-attention.js';
|
|
33
33
|
import { updateTerminalList, handleContextSourcesState, updateEmailAccountList, updateEmailUnreadCounts, handleEmailTestResult, handleEmailAddResult, handleEmailRemoveResult, handleEmailDefaults } from './context-sources.js';
|
|
34
34
|
import { refreshEmailSettings } from './user-settings.js';
|
|
35
35
|
import { handleNotesList, handleNoteCreated, handleNoteUpdated, handleNoteDeleted } from './sticky-notes.js';
|
|
@@ -57,7 +57,7 @@ import { handleRemoteCursorMove, handleRemoteCursorLeave, handleRemoteSelection,
|
|
|
57
57
|
import { showLoopBanner, updateLoopBanner, updateLoopInputVisibility, showRalphApprovalBar, updateRalphApprovalStatus, openRalphPreviewModal, showExecModal, updateExecModalStatus } from './app-loop-ui.js';
|
|
58
58
|
import { showDebateSticky, showDebateConcludeConfirm, showDebateUserFloor, exitDebateFloorMode, exitDebateConcludeMode, exitDebateEndedMode, updateDebateRound, renderDebateUserFloorDone } from './app-debate-ui.js';
|
|
59
59
|
import { handleSkillInstallWs } from './app-skills-install.js';
|
|
60
|
-
import { handleNotificationsState, handleNotificationCreated, handleNotificationDismissed, handleNotificationDismissedAll, showUpdateBanner } from './app-notifications.js';
|
|
60
|
+
import { handleNotificationsState, handleNotificationCreated, handleNotificationDismissed, handleNotificationDismissedAll, showUpdateBanner, autoStartLoginIfNeeded } from './app-notifications.js';
|
|
61
61
|
import { handleDebatePreparing, handleDebateBriefReady, renderDebateBriefReady, handleDebateStarted, renderDebateStarted, handleDebateTurn, handleDebateActivity, handleDebateStream, handleDebateTurnDone, handleDebateCommentQueued, handleDebateCommentInjected, renderDebateCommentInjected, handleDebateResumed, handleDebateEnded, renderDebateEnded, handleDebateError, isDebateActive, renderMcpDebateProposal, renderDebateUserResume } from './debate.js';
|
|
62
62
|
import { handleMentionStart, handleMentionActivity, handleMentionStream, handleMentionDone, handleMentionError, renderMentionUser, renderMentionResponse, renderUserMention } from './mention.js';
|
|
63
63
|
|
|
@@ -443,8 +443,18 @@ export function processMessage(msg) {
|
|
|
443
443
|
if (_modelVal && typeof _modelVal === "object") _modelVal = _modelVal.value || _modelVal.displayName || "";
|
|
444
444
|
var _miUpdate = { currentModels: msg.models || [] };
|
|
445
445
|
if (Object.prototype.hasOwnProperty.call(msg, "model")) {
|
|
446
|
-
|
|
447
|
-
|
|
446
|
+
// Keep the user's existing selection only if it actually belongs to
|
|
447
|
+
// this vendor's model list. Otherwise (e.g. switching to a Codex
|
|
448
|
+
// session while currentModel is still a Claude model) snap to the
|
|
449
|
+
// server-provided default so the chip doesn't show a cross-vendor
|
|
450
|
+
// model.
|
|
451
|
+
var _curModel = store.get('currentModel');
|
|
452
|
+
var _curInList = !!_curModel && (msg.models || []).some(function (m) {
|
|
453
|
+
var v = typeof m === "string" ? m : (m && m.value);
|
|
454
|
+
return v === _curModel;
|
|
455
|
+
});
|
|
456
|
+
if (store.get('vendorSelectionLocked') && _curInList) {
|
|
457
|
+
// Keep the user's existing (valid) selection; only update models list
|
|
448
458
|
} else {
|
|
449
459
|
_miUpdate.currentModel = _modelVal || "";
|
|
450
460
|
}
|
|
@@ -1156,6 +1166,9 @@ export function processMessage(msg) {
|
|
|
1156
1166
|
appendDelta((msg.text || "Authentication required.") + "\n");
|
|
1157
1167
|
setStatus("connected");
|
|
1158
1168
|
if (!store.get('loopActive')) enableMainInput();
|
|
1169
|
+
// Auto-open the login modal terminal when the session can self-login.
|
|
1170
|
+
// No-op otherwise (the auth_required banner remains the manual path).
|
|
1171
|
+
autoStartLoginIfNeeded(msg);
|
|
1159
1172
|
break;
|
|
1160
1173
|
|
|
1161
1174
|
case "rate_limit":
|
|
@@ -1338,6 +1351,19 @@ export function processMessage(msg) {
|
|
|
1338
1351
|
break;
|
|
1339
1352
|
|
|
1340
1353
|
case "term_created":
|
|
1354
|
+
// Login-modal path: the terminal was created with the login command as
|
|
1355
|
+
// its initial input; attach the modal dialog to it (replays scrollback
|
|
1356
|
+
// so the login URL is visible) instead of the sidebar terminal.
|
|
1357
|
+
if (store.get('pendingLoginModal')) {
|
|
1358
|
+
var _lm = store.get('pendingLoginModal');
|
|
1359
|
+
store.set({ pendingLoginModal: null });
|
|
1360
|
+
openTuiModal(msg.id, _lm.slug, {
|
|
1361
|
+
sessionTitle: (_lm.vendor === "codex" ? "Codex" : "Claude") + " login",
|
|
1362
|
+
projectName: _lm.slug,
|
|
1363
|
+
compact: true,
|
|
1364
|
+
});
|
|
1365
|
+
break;
|
|
1366
|
+
}
|
|
1341
1367
|
handleTermCreated(msg);
|
|
1342
1368
|
if (store.get('pendingTermCommand')) {
|
|
1343
1369
|
var cmd = store.get('pendingTermCommand');
|
|
@@ -252,7 +252,7 @@ function showBanner(notif, autoDismissMs) {
|
|
|
252
252
|
removeBanner(banner);
|
|
253
253
|
dismissNotif(notif.id);
|
|
254
254
|
var authMeta = notif.meta || {};
|
|
255
|
-
|
|
255
|
+
startLoginInModal(authMeta.loginCommand || ((authMeta.vendor || "claude") === "codex" ? "codex login --device-auth" : "claude login"), authMeta.vendor || "claude");
|
|
256
256
|
showLoginReminderBanner();
|
|
257
257
|
});
|
|
258
258
|
}
|
|
@@ -311,7 +311,42 @@ function showBanner(notif, autoDismissMs) {
|
|
|
311
311
|
}
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
+
// Open the login terminal as a modal dialog (the same modal used for TUI
|
|
315
|
+
// attention), running the vendor login command. The terminal is created with
|
|
316
|
+
// the command as its initial input, so when the modal attaches it replays the
|
|
317
|
+
// scrollback and the user sees the login URL / prompts immediately.
|
|
318
|
+
// Falls back to the sidebar terminal if the current project slug is unknown
|
|
319
|
+
// (the modal needs a slug to open its own WS to the project).
|
|
320
|
+
function currentProjectSlug() {
|
|
321
|
+
var slug = store.get('currentSlug') || "";
|
|
322
|
+
if (slug) return slug;
|
|
323
|
+
// Fallback: derive from the URL (/p/<slug>/...).
|
|
324
|
+
try {
|
|
325
|
+
var m = (window.location.pathname || "").match(/^\/p\/([a-z0-9_-]+)/);
|
|
326
|
+
if (m) return m[1];
|
|
327
|
+
} catch (e) {}
|
|
328
|
+
return "";
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function startLoginInModal(loginCommand, vendor) {
|
|
332
|
+
if (authReminderVisible) return;
|
|
333
|
+
var ws = getWs();
|
|
334
|
+
if (!ws || ws.readyState !== 1) return;
|
|
335
|
+
var cmd = loginCommand || (vendor === "codex" ? "codex login --device-auth" : "claude login");
|
|
336
|
+
var slug = currentProjectSlug();
|
|
337
|
+
if (!slug) { startLoginCommand(cmd); return; }
|
|
338
|
+
store.set({ pendingLoginModal: { slug: slug, vendor: vendor || "claude" } });
|
|
339
|
+
ws.send(JSON.stringify({
|
|
340
|
+
type: "term_create",
|
|
341
|
+
cols: 100,
|
|
342
|
+
rows: 30,
|
|
343
|
+
initialCommand: cmd + "\n",
|
|
344
|
+
}));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Sidebar-terminal fallback (used when no slug is available for the modal).
|
|
314
348
|
function startLoginCommand(loginCommand) {
|
|
349
|
+
if (authReminderVisible) return;
|
|
315
350
|
var ws = getWs();
|
|
316
351
|
if (!ws || ws.readyState !== 1) return;
|
|
317
352
|
var termCommand = (loginCommand || "claude login") + "\n";
|
|
@@ -320,6 +355,23 @@ function startLoginCommand(loginCommand) {
|
|
|
320
355
|
openTerminal();
|
|
321
356
|
}
|
|
322
357
|
|
|
358
|
+
// Auto-open the login modal and run the login command when the server reports
|
|
359
|
+
// the vendor isn't logged in. The popup terminal runs as the connected user's
|
|
360
|
+
// own OS identity (term_create -> getOsUserInfoForWs), so login always targets
|
|
361
|
+
// the right account — we pop it unconditionally rather than gating on
|
|
362
|
+
// canAutoLogin. Idempotent: startLoginInModal is guarded by authReminderVisible
|
|
363
|
+
// so repeat auth_required events no-op.
|
|
364
|
+
export function autoStartLoginIfNeeded(msg) {
|
|
365
|
+
if (!msg) return false;
|
|
366
|
+
if (authReminderVisible) return false;
|
|
367
|
+
var vendor = msg.vendor || "claude";
|
|
368
|
+
var cmd = msg.loginCommand
|
|
369
|
+
|| (vendor === "codex" ? "codex login --device-auth" : "claude login");
|
|
370
|
+
startLoginInModal(cmd, vendor);
|
|
371
|
+
showLoginReminderBanner();
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
|
|
323
375
|
function showLoginReminderBanner() {
|
|
324
376
|
if (!bannerContainer) return;
|
|
325
377
|
authReminderVisible = true;
|
|
@@ -554,6 +606,11 @@ export function handleNotificationCreated(msg) {
|
|
|
554
606
|
unreadCount = msg.unreadCount;
|
|
555
607
|
updateBadge();
|
|
556
608
|
|
|
609
|
+
// auth_required no longer pops a banner: the login terminal modal opens
|
|
610
|
+
// automatically (see autoStartLoginIfNeeded). Keep the notification in the
|
|
611
|
+
// center (above) for history, but don't show the redundant banner.
|
|
612
|
+
if (notif.type === "auth_required") return;
|
|
613
|
+
|
|
557
614
|
// tui_attention drives the project icon shake and the favicon blink —
|
|
558
615
|
// same visual cues the SDK already produces from pendingPermissions /
|
|
559
616
|
// permission_request messages, so the user notices either kind without
|
|
@@ -147,6 +147,12 @@ export function openTuiModal(terminalId, sourceSlug, info) {
|
|
|
147
147
|
});
|
|
148
148
|
modalEl.classList.remove("hidden");
|
|
149
149
|
|
|
150
|
+
// Compact sizing for short, content-light terminals (e.g. the login
|
|
151
|
+
// wizard) so the box hugs the content instead of leaving a tall empty
|
|
152
|
+
// black area below it. Full-size for live TUI sessions that fill the screen.
|
|
153
|
+
var boxEl = modalEl.querySelector(".tui-modal");
|
|
154
|
+
if (boxEl) boxEl.classList.toggle("tui-modal-compact", !!infoObj.compact);
|
|
155
|
+
|
|
150
156
|
var bodyEl = modalEl.querySelector(".tui-modal-body");
|
|
151
157
|
bodyEl.innerHTML = "";
|
|
152
158
|
modalXterm = new Terminal({
|
|
@@ -36,6 +36,50 @@ function attachMessageProcessor(ctx) {
|
|
|
36
36
|
sm.sendToSession(session, obj);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// Emit the "not logged in" signal (WS message + notification) so the client
|
|
40
|
+
// auto-opens the login modal. Shared by the Claude path (login-prompt text
|
|
41
|
+
// detection) and the Codex path (app-server unauthorized error). Deduped
|
|
42
|
+
// per session to avoid spamming when multiple auth errors arrive in a burst.
|
|
43
|
+
function emitAuthRequired(session) {
|
|
44
|
+
var now = Date.now();
|
|
45
|
+
if (session._lastAuthEmit && (now - session._lastAuthEmit) < 5000) return;
|
|
46
|
+
session._lastAuthEmit = now;
|
|
47
|
+
|
|
48
|
+
var authUser = session.ownerId ? usersModule.findUserById(session.ownerId) : null;
|
|
49
|
+
var authLinuxUser = authUser && authUser.linuxUser ? authUser.linuxUser : null;
|
|
50
|
+
var canAutoLogin = !usersModule.isMultiUser()
|
|
51
|
+
|| !!authLinuxUser
|
|
52
|
+
|| (authUser && authUser.role === "admin");
|
|
53
|
+
var authTitle = session.vendor === "codex" ? "Codex is not logged in." : "Claude Code is not logged in.";
|
|
54
|
+
var loginCommand = session.vendor === "codex"
|
|
55
|
+
? "codex login --device-auth"
|
|
56
|
+
: "claude login";
|
|
57
|
+
var _nmLogin = getNotificationsModule();
|
|
58
|
+
sendAndRecord(session, {
|
|
59
|
+
type: "auth_required",
|
|
60
|
+
text: authTitle,
|
|
61
|
+
vendor: session.vendor || "claude",
|
|
62
|
+
loginCommand: loginCommand,
|
|
63
|
+
linuxUser: authLinuxUser,
|
|
64
|
+
canAutoLogin: canAutoLogin,
|
|
65
|
+
});
|
|
66
|
+
if (_nmLogin) {
|
|
67
|
+
_nmLogin.notify("auth_required", {
|
|
68
|
+
title: authTitle,
|
|
69
|
+
body: "Open a terminal, then click the URL and follow the instructions.",
|
|
70
|
+
slug: slug,
|
|
71
|
+
sessionId: session.localId,
|
|
72
|
+
ownerId: session.ownerId || null,
|
|
73
|
+
vendor: session.vendor || "claude",
|
|
74
|
+
loginCommand: loginCommand,
|
|
75
|
+
linuxUser: authLinuxUser,
|
|
76
|
+
canAutoLogin: canAutoLogin,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// Reset CLI session so the next query starts fresh with new auth.
|
|
80
|
+
session.cliSessionId = null;
|
|
81
|
+
}
|
|
82
|
+
|
|
39
83
|
function getModelsForVendor(vendor) {
|
|
40
84
|
if (vendor && sm.modelsByVendor && sm.modelsByVendor[vendor]) return sm.modelsByVendor[vendor];
|
|
41
85
|
return sm.availableModels || [];
|
|
@@ -447,40 +491,7 @@ function attachMessageProcessor(ctx) {
|
|
|
447
491
|
// Detect "Not logged in / Please run /login" from SDK.
|
|
448
492
|
// This is a short canned response with zero cost, not actual AI output.
|
|
449
493
|
if (isLoginPrompt) {
|
|
450
|
-
|
|
451
|
-
var authLinuxUser = authUser && authUser.linuxUser ? authUser.linuxUser : null;
|
|
452
|
-
var canAutoLogin = !usersModule.isMultiUser()
|
|
453
|
-
|| !!authLinuxUser
|
|
454
|
-
|| (authUser && authUser.role === "admin");
|
|
455
|
-
var authTitle = session.vendor === "codex" ? "Codex is not logged in." : "Claude Code is not logged in.";
|
|
456
|
-
var loginCommand = session.vendor === "codex"
|
|
457
|
-
? "codex login --device-auth"
|
|
458
|
-
: "claude login";
|
|
459
|
-
var _nmLogin = getNotificationsModule();
|
|
460
|
-
var authMsg = {
|
|
461
|
-
type: "auth_required",
|
|
462
|
-
text: authTitle,
|
|
463
|
-
vendor: session.vendor || "claude",
|
|
464
|
-
loginCommand: loginCommand,
|
|
465
|
-
linuxUser: authLinuxUser,
|
|
466
|
-
canAutoLogin: canAutoLogin,
|
|
467
|
-
};
|
|
468
|
-
sendAndRecord(session, authMsg);
|
|
469
|
-
if (_nmLogin) {
|
|
470
|
-
_nmLogin.notify("auth_required", {
|
|
471
|
-
title: authTitle,
|
|
472
|
-
body: "Open a terminal, then click the URL and follow the instructions.",
|
|
473
|
-
slug: slug,
|
|
474
|
-
sessionId: session.localId,
|
|
475
|
-
ownerId: session.ownerId || null,
|
|
476
|
-
vendor: session.vendor || "claude",
|
|
477
|
-
loginCommand: loginCommand,
|
|
478
|
-
linuxUser: authLinuxUser,
|
|
479
|
-
canAutoLogin: canAutoLogin,
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
// Reset CLI session so next query starts fresh with new auth
|
|
483
|
-
session.cliSessionId = null;
|
|
494
|
+
emitAuthRequired(session);
|
|
484
495
|
}
|
|
485
496
|
sendAndRecord(session, { type: "done", code: 0 });
|
|
486
497
|
var _donePreviewText = (session.responsePreview || "").replace(/\s+/g, " ").trim();
|
|
@@ -751,6 +762,14 @@ function attachMessageProcessor(ctx) {
|
|
|
751
762
|
message: parsed.message || "",
|
|
752
763
|
});
|
|
753
764
|
|
|
765
|
+
} else if (parsed.yokeType === "auth_required") {
|
|
766
|
+
// Vendor adapter signalled the session isn't authenticated (e.g. Codex
|
|
767
|
+
// app-server returned an unauthorized/token-revoked error). Trigger the
|
|
768
|
+
// same login flow as the Claude login-prompt path.
|
|
769
|
+
session.isProcessing = false;
|
|
770
|
+
onProcessingChanged();
|
|
771
|
+
emitAuthRequired(session);
|
|
772
|
+
|
|
754
773
|
} else if (parsed.yokeType === "model_refusal") {
|
|
755
774
|
// Model declined the request. "fallback" => the CLI retried on another
|
|
756
775
|
// model; "no_fallback" => the turn ended with a refusal.
|
package/lib/whats-new-content.js
CHANGED
|
@@ -27,6 +27,7 @@ var ENTRIES = [
|
|
|
27
27
|
title: "TUI becomes the default for Claude sessions",
|
|
28
28
|
publishedAt: "2026-05-25",
|
|
29
29
|
image: null,
|
|
30
|
+
popup: false, // superseded by "GUI is back"; keep in the home feed only
|
|
30
31
|
summary: "Anthropic is splitting Claude billing into Interactive and Programmatic buckets on June 15. To keep working with your existing plan, Clay opens Claude sessions in the embedded claude CLI by default from that date.",
|
|
31
32
|
body: '' +
|
|
32
33
|
'<p><strong>2026-06-15 Anthropic billing change.</strong> Claude subscriptions split into two buckets:</p>' +
|
|
@@ -55,6 +56,7 @@ var ENTRIES = [
|
|
|
55
56
|
title: "Mates leaves the official feature set",
|
|
56
57
|
publishedAt: "2026-05-25",
|
|
57
58
|
image: null,
|
|
59
|
+
popup: false, // superseded by "GUI is back"; keep in the home feed only
|
|
58
60
|
summary: "June 15 will be Mates' last day as a first-class Clay feature. It continues as a separate standalone project so Clay can focus fully on being a developer tool.",
|
|
59
61
|
body: '' +
|
|
60
62
|
'<p>On <strong>2026-06-15</strong>, Mates leaves Clay\'s official feature set. Your existing Mates and their knowledge files will remain on disk; we will share migration instructions and a link to the new standalone project before the cutover.</p>' +
|
|
@@ -63,6 +65,20 @@ var ENTRIES = [
|
|
|
63
65
|
'<p>We are formally positioning Clay as a <strong>developer tool</strong>. From here on, every roadmap decision is judged by a single question: does it make Clay the sleekest, sharpest workspace on the AI-coding frontier? Mates no longer fits that bar cleanly, so it gets its own home where it can evolve on its own terms.</p>' +
|
|
64
66
|
'<p>Thanks for being here. Clay only gets sharper from here.</p>',
|
|
65
67
|
},
|
|
68
|
+
{
|
|
69
|
+
id: "2026-07-gui-is-back",
|
|
70
|
+
title: "GUI is back as the default",
|
|
71
|
+
publishedAt: "2026-07-01",
|
|
72
|
+
image: null,
|
|
73
|
+
summary: "Anthropic reversed the June 15 billing split, so SDK usage is no longer billed separately. Clay opens Claude sessions in the GUI chat again by default. Mates is now off by default as it winds down.",
|
|
74
|
+
body: '' +
|
|
75
|
+
'<p><strong>Good news: Anthropic walked back the June 15 billing change.</strong> The planned Interactive vs. Programmatic split (which would have charged Claude Agent SDK usage at full API rates) is off the table, so the reason we moved everyone to the TUI is gone.</p>' +
|
|
76
|
+
'<h3>GUI is the default again</h3>' +
|
|
77
|
+
'<p>New Claude sessions open in the <strong>GUI chat</strong> (SDK) by default, the way they did before. The embedded <code>claude</code> <strong>TUI</strong> is still there whenever you want it — toggle <strong>"Open Claude as terminal (TUI)"</strong> in Settings.</p>' +
|
|
78
|
+
'<h3>Mates is off by default</h3>' +
|
|
79
|
+
'<p>As announced, <strong>Mates</strong> is no longer a first-class Clay feature. It is now <strong>off by default</strong> so Clay stays focused as a developer tool. Your existing Mates and their knowledge files remain on disk — if you still use them, re-enable Mates in <strong>Settings</strong>.</p>' +
|
|
80
|
+
'<p>Clay only gets sharper from here.</p>',
|
|
81
|
+
},
|
|
66
82
|
];
|
|
67
83
|
|
|
68
84
|
module.exports = { ENTRIES: ENTRIES };
|
package/lib/whats-new.js
CHANGED
|
@@ -26,6 +26,9 @@ function getStateForUser(userId) {
|
|
|
26
26
|
var seen = (typeof users.getWhatsNewSeenIds === "function" && userId) ? users.getWhatsNewSeenIds(userId) : [];
|
|
27
27
|
var unseenIds = [];
|
|
28
28
|
for (var i = 0; i < entries.length; i++) {
|
|
29
|
+
// popup:false entries stay in the home feed (entries) as history but never
|
|
30
|
+
// auto-pop in the carousel, so superseded announcements don't resurface.
|
|
31
|
+
if (entries[i] && entries[i].popup === false) continue;
|
|
29
32
|
if (entries[i] && entries[i].id && seen.indexOf(entries[i].id) === -1) {
|
|
30
33
|
unseenIds.push(entries[i].id);
|
|
31
34
|
}
|
|
@@ -145,6 +145,16 @@ function normalizePlanStatus(status) {
|
|
|
145
145
|
return "pending";
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
// Detect Codex "not logged in" errors. Codex surfaces auth failures several
|
|
149
|
+
// ways depending on transport: a clean error event with
|
|
150
|
+
// codexErrorInfo:"unauthorized", or a turn/failed / item error whose message
|
|
151
|
+
// carries a 401 / token-revoked / missing-bearer / "sign in again" string.
|
|
152
|
+
// Callers map a match to the neutral auth_required yokeType.
|
|
153
|
+
function isCodexAuthError(text, errObj) {
|
|
154
|
+
if (errObj && errObj.codexErrorInfo === "unauthorized") return true;
|
|
155
|
+
return /sign in again|token[_ ]?revoked|invalidated oauth|missing bearer|unauthorized|\b401\b/i.test(String(text || ""));
|
|
156
|
+
}
|
|
157
|
+
|
|
148
158
|
function extractPromptSuggestion(params) {
|
|
149
159
|
if (!params) return "";
|
|
150
160
|
if (typeof params.suggestion === "string") return params.suggestion;
|
|
@@ -226,9 +236,14 @@ function flattenEvent(notification, state) {
|
|
|
226
236
|
}
|
|
227
237
|
|
|
228
238
|
if (method === "turn/failed") {
|
|
239
|
+
var tfMsg = params.error ? params.error.message : "Turn failed";
|
|
240
|
+
if (isCodexAuthError(tfMsg, params.error)) {
|
|
241
|
+
events.push({ yokeType: "auth_required", vendor: "codex" });
|
|
242
|
+
return events;
|
|
243
|
+
}
|
|
229
244
|
events.push({
|
|
230
245
|
yokeType: "error",
|
|
231
|
-
text:
|
|
246
|
+
text: tfMsg,
|
|
232
247
|
});
|
|
233
248
|
return events;
|
|
234
249
|
}
|
|
@@ -525,9 +540,14 @@ function flattenEvent(notification, state) {
|
|
|
525
540
|
|
|
526
541
|
// Error item
|
|
527
542
|
if (item.type === "error") {
|
|
543
|
+
var ieMsg = item.message || "Unknown error";
|
|
544
|
+
if (isCodexAuthError(ieMsg, item)) {
|
|
545
|
+
events.push({ yokeType: "auth_required", vendor: "codex" });
|
|
546
|
+
return events;
|
|
547
|
+
}
|
|
528
548
|
events.push({
|
|
529
549
|
yokeType: "error",
|
|
530
|
-
text:
|
|
550
|
+
text: ieMsg,
|
|
531
551
|
});
|
|
532
552
|
return events;
|
|
533
553
|
}
|
|
@@ -551,6 +571,20 @@ function flattenEvent(notification, state) {
|
|
|
551
571
|
return events;
|
|
552
572
|
}
|
|
553
573
|
|
|
574
|
+
// Top-level error event. Codex signals "not logged in" via an unauthorized /
|
|
575
|
+
// token-revoked error (not a login-prompt message like Claude), so map it to
|
|
576
|
+
// the neutral auth_required yokeType to drive the login flow.
|
|
577
|
+
if (method === "error" && params && params.error) {
|
|
578
|
+
var cErr = params.error;
|
|
579
|
+
var cErrMsg = cErr.message || "Codex error";
|
|
580
|
+
if (isCodexAuthError(cErrMsg, cErr)) {
|
|
581
|
+
events.push({ yokeType: "auth_required", vendor: "codex" });
|
|
582
|
+
return events;
|
|
583
|
+
}
|
|
584
|
+
events.push({ yokeType: "error", text: cErrMsg });
|
|
585
|
+
return events;
|
|
586
|
+
}
|
|
587
|
+
|
|
554
588
|
// Unknown event type - pass through
|
|
555
589
|
console.log("[yoke/codex] UNHANDLED event:", method, JSON.stringify(params).substring(0, 200));
|
|
556
590
|
events.push({
|
|
@@ -922,10 +956,10 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
922
956
|
if (!isCancelled() && e.name !== "AbortError") {
|
|
923
957
|
console.error("[yoke/codex] runQueryLoop error:", e.message || e);
|
|
924
958
|
console.error("[yoke/codex] stack:", e.stack || "(no stack)");
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
959
|
+
var loopErrMsg = e.message || String(e);
|
|
960
|
+
pushEvent(isCodexAuthError(loopErrMsg)
|
|
961
|
+
? { yokeType: "auth_required", vendor: "codex" }
|
|
962
|
+
: { yokeType: "error", text: loopErrMsg });
|
|
929
963
|
}
|
|
930
964
|
}
|
|
931
965
|
|
|
@@ -1047,7 +1081,19 @@ function createCodexAdapter(opts) {
|
|
|
1047
1081
|
var _cwd = (opts && opts.cwd) || process.cwd();
|
|
1048
1082
|
var _slug = (opts && opts.slug) || "";
|
|
1049
1083
|
var _defaultInitOpts = Object.assign({}, opts || {});
|
|
1050
|
-
|
|
1084
|
+
// Codex models are a fixed list (the app-server doesn't enumerate them), so
|
|
1085
|
+
// model listing must not depend on a successful app-server init — otherwise a
|
|
1086
|
+
// slow/failed `initialize` leaves the picker empty and the chip shows the
|
|
1087
|
+
// previous vendor's model.
|
|
1088
|
+
var CODEX_MODELS = [
|
|
1089
|
+
"gpt-5.5",
|
|
1090
|
+
"gpt-5.4",
|
|
1091
|
+
"gpt-5.4-mini",
|
|
1092
|
+
"gpt-5.3-codex",
|
|
1093
|
+
"gpt-5.3-codex-spark",
|
|
1094
|
+
"gpt-5.2",
|
|
1095
|
+
];
|
|
1096
|
+
var _cachedModels = CODEX_MODELS.slice();
|
|
1051
1097
|
var _appServer = null;
|
|
1052
1098
|
var _initPromise = null;
|
|
1053
1099
|
var _shutdownPromise = null;
|
|
@@ -1297,16 +1343,9 @@ function createCodexAdapter(opts) {
|
|
|
1297
1343
|
throw createShutdownError();
|
|
1298
1344
|
}
|
|
1299
1345
|
|
|
1300
|
-
console.log("[codex] App-server initialized, models:
|
|
1346
|
+
console.log("[codex] App-server initialized, models: " + CODEX_MODELS.join(", "));
|
|
1301
1347
|
|
|
1302
|
-
_cachedModels =
|
|
1303
|
-
"gpt-5.5",
|
|
1304
|
-
"gpt-5.4",
|
|
1305
|
-
"gpt-5.4-mini",
|
|
1306
|
-
"gpt-5.3-codex",
|
|
1307
|
-
"gpt-5.3-codex-spark",
|
|
1308
|
-
"gpt-5.2",
|
|
1309
|
-
];
|
|
1348
|
+
_cachedModels = CODEX_MODELS.slice();
|
|
1310
1349
|
|
|
1311
1350
|
// Discover skills: built-in Codex skills + Claude skills
|
|
1312
1351
|
var skillNames = [];
|
|
@@ -1361,7 +1400,8 @@ function createCodexAdapter(opts) {
|
|
|
1361
1400
|
},
|
|
1362
1401
|
|
|
1363
1402
|
supportedModels: function() {
|
|
1364
|
-
return
|
|
1403
|
+
// Fixed list; return it without requiring a live app-server init.
|
|
1404
|
+
return Promise.resolve(CODEX_MODELS.slice());
|
|
1365
1405
|
},
|
|
1366
1406
|
|
|
1367
1407
|
createToolServer: function(def) {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
var { spawn } = require("child_process");
|
|
8
8
|
var readline = require("readline");
|
|
9
9
|
var path = require("path");
|
|
10
|
+
var fs = require("fs");
|
|
10
11
|
var { createRequire } = require("module");
|
|
11
12
|
|
|
12
13
|
// --- Find the codex binary path ---
|
|
@@ -48,7 +49,17 @@ function findCodexPath() {
|
|
|
48
49
|
var platformPkgJson = codexRequire.resolve(platformPkg + "/package.json");
|
|
49
50
|
var vendorRoot = path.join(path.dirname(platformPkgJson), "vendor");
|
|
50
51
|
var binaryName = process.platform === "win32" ? "codex.exe" : "codex";
|
|
51
|
-
|
|
52
|
+
// The binary layout changed across versions: 0.142+ ships it under
|
|
53
|
+
// vendor/<triple>/bin/, older builds used vendor/<triple>/codex/.
|
|
54
|
+
// Try the known layouts and return the first that exists.
|
|
55
|
+
var candidates = [
|
|
56
|
+
path.join(vendorRoot, triple, "bin", binaryName),
|
|
57
|
+
path.join(vendorRoot, triple, "codex", binaryName),
|
|
58
|
+
];
|
|
59
|
+
for (var i = 0; i < candidates.length; i++) {
|
|
60
|
+
if (fs.existsSync(candidates[i])) return candidates[i];
|
|
61
|
+
}
|
|
62
|
+
throw new Error("codex binary not found in any known layout under " + path.join(vendorRoot, triple));
|
|
52
63
|
} catch (e) {
|
|
53
64
|
throw new Error("Could not find codex binary: " + e.message);
|
|
54
65
|
}
|
|
@@ -153,6 +164,7 @@ CodexAppServer.prototype.start = function() {
|
|
|
153
164
|
while (lines.length > 1) {
|
|
154
165
|
var line = lines.shift();
|
|
155
166
|
if (line.trim()) console.log("[codex-app-server stderr]", line);
|
|
167
|
+
self._maybeSignalAuthError(line);
|
|
156
168
|
}
|
|
157
169
|
self._stderrBuf = lines[0] || "";
|
|
158
170
|
});
|
|
@@ -211,6 +223,23 @@ CodexAppServer.prototype._handleMessage = function(msg) {
|
|
|
211
223
|
}
|
|
212
224
|
};
|
|
213
225
|
|
|
226
|
+
// The definitive "not logged in" signal from Codex is a 401 on its own
|
|
227
|
+
// responses endpoint, which only appears on stderr (not as a JSON-RPC error).
|
|
228
|
+
// When we see it, synthesize an error event so the adapter maps it to the
|
|
229
|
+
// neutral auth_required yokeType. Deduped so a burst of 401 retries collapses.
|
|
230
|
+
CodexAppServer.prototype._maybeSignalAuthError = function(line) {
|
|
231
|
+
if (!this.eventHandler || !line || this._authSignalSent) return;
|
|
232
|
+
var isAuth = /missing bearer|token[_ ]?revoked|invalidated oauth|please sign in again/i.test(line)
|
|
233
|
+
|| (/\b401\b/.test(line) && /unauthorized|responses|openai\.com|chatgpt\.com/i.test(line));
|
|
234
|
+
if (!isAuth) return;
|
|
235
|
+
this._authSignalSent = true;
|
|
236
|
+
var self = this;
|
|
237
|
+
setTimeout(function () { self._authSignalSent = false; }, 15000);
|
|
238
|
+
try {
|
|
239
|
+
this.eventHandler({ method: "error", params: { error: { codexErrorInfo: "unauthorized", message: line } } });
|
|
240
|
+
} catch (e) {}
|
|
241
|
+
};
|
|
242
|
+
|
|
214
243
|
// Send a JSON-RPC request (expects a response)
|
|
215
244
|
CodexAppServer.prototype.send = function(method, params, timeoutMs) {
|
|
216
245
|
var self = this;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clay-server",
|
|
3
|
-
"version": "2.45.0-beta.
|
|
3
|
+
"version": "2.45.0-beta.4",
|
|
4
4
|
"description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"clay-server": "./bin/cli.js",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@anthropic-ai/claude-agent-sdk": "^0.3.196",
|
|
52
52
|
"@lydell/node-pty": "^1.2.0-beta.3",
|
|
53
|
-
"@openai/codex": "^0.
|
|
53
|
+
"@openai/codex": "^0.142.4",
|
|
54
54
|
"imapflow": "^1.3.1",
|
|
55
55
|
"nodemailer": "^6.10.1",
|
|
56
56
|
"qrcode-terminal": "^0.12.0",
|