clay-server 2.32.0-beta.1 → 2.32.0-beta.11

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.
@@ -0,0 +1,18 @@
1
+ var CODEX_DEFAULTS = {
2
+ approval: "on-failure",
3
+ sandbox: "danger-full-access",
4
+ webSearch: "live",
5
+ };
6
+
7
+ function getCodexConfig(sm) {
8
+ return {
9
+ approval: (sm && sm.codexApproval) || CODEX_DEFAULTS.approval,
10
+ sandbox: (sm && sm.codexSandbox) || CODEX_DEFAULTS.sandbox,
11
+ webSearch: (sm && sm.codexWebSearch) || CODEX_DEFAULTS.webSearch,
12
+ };
13
+ }
14
+
15
+ module.exports = {
16
+ CODEX_DEFAULTS: CODEX_DEFAULTS,
17
+ getCodexConfig: getCodexConfig,
18
+ };
@@ -3,6 +3,7 @@ var path = require("path");
3
3
  var usersModule = require("./users");
4
4
  var userPresence = require("./user-presence");
5
5
  var emailAccounts = require("./email-accounts");
6
+ var { getCodexConfig } = require("./codex-defaults");
6
7
 
7
8
  /**
8
9
  * Attach connection/disconnection handlers to a project context.
@@ -84,10 +85,9 @@ function attachConnection(ctx) {
84
85
  var project = getProject();
85
86
  var ownerLocked = !!(osUsers && osUsers.length > 0 && /^\/home\/[^/]+\//.test(cwd));
86
87
  sendTo(ws, { type: "info", cwd: cwd, slug: slug, project: title || project, version: currentVersion, debug: !!debug, dangerouslySkipPermissions: dangerouslySkipPermissions, osUsers: osUsers, lanHost: lanHost, projectCount: _filteredProjects.length, projects: _filteredProjects, projectOwnerId: projectOwnerId, ownerLocked: ownerLocked });
87
- var latestVersion = getLatestVersion();
88
- if (latestVersion && ws._clayUser && ws._clayUser.role === "admin") {
89
- sendTo(ws, { type: "update_available", version: latestVersion });
90
- }
88
+ // Update notifications are pushed on a scheduled interval (see
89
+ // scheduleUpdateBroadcast). We no longer push on connect to avoid
90
+ // re-triggering the banner on every page refresh.
91
91
  if (sm.slashCommands) {
92
92
  sendTo(ws, { type: "slash_commands", commands: sm.slashCommands });
93
93
  }
@@ -96,6 +96,7 @@ function attachConnection(ctx) {
96
96
  sendTo(ws, { type: "model_info", model: sm.currentModel, models: sm.availableModels || [], vendor: sm.defaultVendor || "claude", availableVendors: sm.availableVendors || [], installedVendors: sm.installedVendors || [] });
97
97
  }
98
98
  sendTo(ws, { type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
99
+ sendTo(ws, Object.assign({ type: "codex_config" }, getCodexConfig(sm)));
99
100
  sendTo(ws, { type: "term_list", terminals: tm.list() });
100
101
  // Context sources sent after session is resolved (per-session storage)
101
102
  // Send email accounts list for context sources picker
@@ -601,7 +601,9 @@ function attachDebate(ctx) {
601
601
  var digests = ctx.loadMateDigests(mateCtx, debate.moderatorId, debate.topic);
602
602
 
603
603
  var briefText = "";
604
+ var _modMate = matesModule.getMate(mateCtx, debate.moderatorId);
604
605
  ctx.sdk.createMentionSession({
606
+ vendor: _modMate ? _modMate.vendor : null,
605
607
  claudeMd: claudeMd,
606
608
  initialContext: digests,
607
609
  initialMessage: quickBriefPrompt,
@@ -794,7 +796,9 @@ function attachDebate(ctx) {
794
796
  var digests = ctx.loadMateDigests(mateCtx, debate.moderatorId, debate.topic);
795
797
  var moderatorContext = buildModeratorContext(debate) + digests;
796
798
 
799
+ var _modMate2 = matesModule.getMate(mateCtx, debate.moderatorId);
797
800
  ctx.sdk.createMentionSession({
801
+ vendor: _modMate2 ? _modMate2.vendor : null,
798
802
  claudeMd: claudeMd,
799
803
  initialContext: moderatorContext,
800
804
  initialMessage: "Begin the debate on: " + debate.topic,
@@ -987,7 +991,9 @@ function attachDebate(ctx) {
987
991
  historyContext += "---";
988
992
  }
989
993
 
994
+ var _panMate = matesModule.getMate(debate.mateCtx, mateId);
990
995
  ctx.sdk.createMentionSession({
996
+ vendor: _panMate ? _panMate.vendor : null,
991
997
  claudeMd: claudeMd,
992
998
  initialContext: panelistContext + historyContext,
993
999
  initialMessage: "The moderator addresses you:\n\n" + moderatorText,
@@ -1521,7 +1527,9 @@ function attachDebate(ctx) {
1521
1527
  }
1522
1528
  moderatorContext += "---\n";
1523
1529
 
1530
+ var _modMate3 = matesModule.getMate(mateCtx, debate.moderatorId);
1524
1531
  ctx.sdk.createMentionSession({
1532
+ vendor: _modMate3 ? _modMate3.vendor : null,
1525
1533
  claudeMd: claudeMd,
1526
1534
  initialContext: moderatorContext,
1527
1535
  initialMessage: resumePrompt,
@@ -4,6 +4,7 @@ var crypto = require("crypto");
4
4
 
5
5
  function attachMateInteraction(ctx) {
6
6
  var cwd = ctx.cwd;
7
+ var slug = ctx.slug || "";
7
8
  var sm = ctx.sm;
8
9
  var sdk = ctx.sdk;
9
10
  var send = ctx.send;
@@ -21,6 +22,7 @@ function attachMateInteraction(ctx) {
21
22
  var loadMateDigests = ctx.loadMateDigests;
22
23
  var updateMemorySummary = ctx.updateMemorySummary;
23
24
  var initMemorySummary = ctx.initMemorySummary;
25
+ var getNotificationsModule = ctx.getNotificationsModule || function () { return null; };
24
26
  // checkForDmDebateBrief is accessed via ctx at call time because
25
27
  // it comes from the debate module initialized after this one.
26
28
 
@@ -104,6 +106,30 @@ function attachMateInteraction(ctx) {
104
106
  return lines.join("\n") + "\n\n";
105
107
  }
106
108
 
109
+ function getHostMateId() {
110
+ if (typeof slug === "string" && slug.indexOf("mate-") === 0) {
111
+ return slug.substring(5) || null;
112
+ }
113
+ return null;
114
+ }
115
+
116
+ function notifyMentionResponse(session, mentionMateId, mentionMateName, fullText) {
117
+ var notificationsModule = getNotificationsModule();
118
+ var hostMateId = getHostMateId();
119
+ var preview = (fullText || "").replace(/\s+/g, " ").trim();
120
+ if (preview.length > 140) preview = preview.substring(0, 140) + "...";
121
+ if (!notificationsModule || !hostMateId) return;
122
+ notificationsModule.notify("mention_response", {
123
+ title: (mentionMateName || "Mate") + " responded",
124
+ preview: preview,
125
+ slug: slug,
126
+ sessionId: session.localId,
127
+ mateId: hostMateId,
128
+ avatarMateId: mentionMateId || null,
129
+ ownerId: session.ownerId || null,
130
+ });
131
+ }
132
+
107
133
  // --- Shared digest worker: one reusable Haiku session for gate+digest ---
108
134
  // Combines gate check and digest generation into a single prompt,
109
135
  // processes jobs sequentially from a queue, reuses the session across calls.
@@ -136,12 +162,19 @@ function attachMateInteraction(ctx) {
136
162
  } catch (e) {}
137
163
 
138
164
  // Combined gate + digest in one prompt (saves a full round-trip vs separate gate)
139
- var prompt = [
165
+ var promptParts = [
140
166
  "[SYSTEM: Memory Gate + Digest]",
141
167
  "You are a memory system for an AI Mate (role: " + (mateRole || "assistant") + ").",
142
168
  "",
143
- "Conversation (" + job.type + "):",
144
- job.conversationContent,
169
+ ];
170
+ if (job.priorSummary) {
171
+ promptParts.push("Prior conversation context (memory summary so far):");
172
+ promptParts.push(job.priorSummary);
173
+ promptParts.push("");
174
+ }
175
+ promptParts.push("Conversation (" + job.type + "):");
176
+ promptParts.push(job.conversationContent);
177
+ var prompt = promptParts.concat([
145
178
  "",
146
179
  "STEP 1: Should this be saved to memory?",
147
180
  'Answer "no" ONLY if the entire conversation is trivial (e.g. just "hi"/"hello").',
@@ -171,7 +204,7 @@ function attachMateInteraction(ctx) {
171
204
  "user_observations: OPTIONAL array. Include ONLY if you noticed meaningful patterns about the USER themselves (not the topic).",
172
205
  "Categories: pattern (repeated behavior 2+ times), decision (explicit choice with reasoning), reaction (emotional/attitude signal), preference (tool/style/communication preference).",
173
206
  "Omit the field entirely if nothing notable about the user.",
174
- ].join("\n");
207
+ ]).join("\n");
175
208
 
176
209
  function handleResult(text) {
177
210
  var cleaned = text.trim();
@@ -225,9 +258,14 @@ function attachMateInteraction(ctx) {
225
258
  }
226
259
  }
227
260
 
228
- updateMemorySummary(job.mateCtx, job.mateId, digestObj);
229
- maybeSynthesizeUserProfile(job.mateCtx, job.mateId);
230
- if (job.onDone) job.onDone();
261
+ // Skip summary update for failed parses -- the fallback digest would degrade quality
262
+ if (digestObj.topic !== "parse_failed") {
263
+ updateMemorySummary(job.mateCtx, job.mateId, digestObj);
264
+ maybeSynthesizeUserProfile(job.mateCtx, job.mateId);
265
+ if (job.onDone) job.onDone();
266
+ } else {
267
+ if (job.onError) job.onError(new Error("parse_failed"));
268
+ }
231
269
  processDigestQueue();
232
270
  }
233
271
 
@@ -249,7 +287,7 @@ function attachMateInteraction(ctx) {
249
287
  console.error("[digest-worker] Error:", err);
250
288
  _digestWorker = null;
251
289
  _digestWorkerTurns = 0;
252
- if (job.onDone) job.onDone();
290
+ if (job.onError) job.onError(err);
253
291
  processDigestQueue();
254
292
  },
255
293
  });
@@ -265,11 +303,11 @@ function attachMateInteraction(ctx) {
265
303
  onError: function (err) {
266
304
  console.error("[digest-worker] Create error:", err);
267
305
  _digestWorker = null;
268
- if (job.onDone) job.onDone();
306
+ if (job.onError) job.onError(err);
269
307
  processDigestQueue();
270
308
  },
271
- }).then(function (ws) { _digestWorker = ws; _digestWorkerTurns = 1; }).catch(function () {
272
- if (job.onDone) job.onDone();
309
+ }).then(function (ws) { _digestWorker = ws; _digestWorkerTurns = 1; }).catch(function (err) {
310
+ if (job.onError) job.onError(err || new Error("digest worker creation failed"));
273
311
  processDigestQueue();
274
312
  });
275
313
  }
@@ -303,10 +341,14 @@ function attachMateInteraction(ctx) {
303
341
  type: "mention",
304
342
  conversationContent: conversationContent,
305
343
  onDone: function () { mentionSession._digesting = false; },
344
+ onError: function () { mentionSession._digesting = false; },
306
345
  });
307
346
  }
308
347
 
309
- // Digest DM turn for mate projects - uses shared digest worker
348
+ // Digest DM turn for mate projects - uses shared digest worker.
349
+ // Delta-based: only collects new turns since the last successful digest.
350
+ // Concurrency debounce: turns that arrive while a digest is in-flight
351
+ // are naturally batched into the next flush.
310
352
  var _dmDigestPending = false;
311
353
  function digestDmTurn(session, responsePreview) {
312
354
  if (!isMate || _dmDigestPending) return;
@@ -314,11 +356,26 @@ function attachMateInteraction(ctx) {
314
356
  var mateCtx = matesModule.buildMateCtx(projectOwnerId);
315
357
  if (!matesModule.isMate(mateCtx, mateId)) return;
316
358
 
317
- // Collect full conversation from session history (all user + mate turns)
359
+ // Track digest index per session so switching sessions doesn't misalign.
360
+ // On resumed sessions (after restart), recover index from the last
361
+ // digest_checkpoint entry in history so undigested turns aren't lost.
362
+ if (typeof session._dmLastDigestedIndex !== "number") {
363
+ session._dmLastDigestedIndex = 0;
364
+ for (var ci = session.history.length - 1; ci >= 0; ci--) {
365
+ if (session.history[ci].type === "digest_checkpoint") {
366
+ session._dmLastDigestedIndex = session.history[ci].digestedIndex;
367
+ break;
368
+ }
369
+ }
370
+ }
371
+
372
+ // Collect only new turns since the last successful digest
318
373
  var conversationParts = [];
319
374
  var totalLen = 0;
320
375
  var CONV_CAP = 6000;
321
- for (var hi = 0; hi < session.history.length; hi++) {
376
+ var startIndex = session._dmLastDigestedIndex;
377
+ for (var hi = startIndex; hi < session.history.length; hi++) {
378
+ if (totalLen >= CONV_CAP) break;
322
379
  var entry = session.history[hi];
323
380
  if (entry.type === "user_message" && entry.text) {
324
381
  var uText = entry.text;
@@ -335,8 +392,8 @@ function attachMateInteraction(ctx) {
335
392
  conversationParts.push("Mate: " + aText);
336
393
  totalLen += aText.length;
337
394
  }
338
- if (totalLen >= CONV_CAP) break;
339
395
  }
396
+ // If the latest response hasn't landed in history yet, append it
340
397
  var lastResponseText = responsePreview || "";
341
398
  if (lastResponseText && conversationParts.length > 0) {
342
399
  var lastPart = conversationParts[conversationParts.length - 1];
@@ -362,14 +419,34 @@ function attachMateInteraction(ctx) {
362
419
  });
363
420
  }
364
421
 
422
+ // Read existing summary to give the digest worker context for delta content
423
+ var priorSummary = "";
424
+ try {
425
+ if (fs.existsSync(summaryFile)) {
426
+ priorSummary = fs.readFileSync(summaryFile, "utf8").trim();
427
+ }
428
+ } catch (e) {}
429
+
365
430
  _dmDigestPending = true;
431
+ var snapshotIndex = session.history.length;
366
432
 
367
433
  enqueueDigest({
368
434
  mateCtx: mateCtx,
369
435
  mateId: mateId,
370
436
  type: "dm",
437
+ priorSummary: priorSummary || "",
371
438
  conversationContent: conversationParts.join("\n"),
372
- onDone: function () { _dmDigestPending = false; },
439
+ onDone: function () {
440
+ session._dmLastDigestedIndex = snapshotIndex;
441
+ // Persist checkpoint so resumed sessions know where to continue
442
+ var checkpoint = { type: "digest_checkpoint", digestedIndex: snapshotIndex };
443
+ session.history.push(checkpoint);
444
+ sm.appendToSessionFile(session, checkpoint);
445
+ _dmDigestPending = false;
446
+ },
447
+ onError: function () {
448
+ _dmDigestPending = false;
449
+ },
373
450
  });
374
451
  }
375
452
 
@@ -445,6 +522,7 @@ function attachMateInteraction(ctx) {
445
522
  }
446
523
 
447
524
  session._mentionInProgress = true;
525
+ session._mentionActiveMateId = msg.mateId;
448
526
 
449
527
  // Send mention start indicator
450
528
  sendToSession(session.localId, {
@@ -478,6 +556,7 @@ function attachMateInteraction(ctx) {
478
556
  },
479
557
  onDone: function (fullText) {
480
558
  session._mentionInProgress = false;
559
+ session._mentionActiveMateId = null;
481
560
 
482
561
  // Save mention response to session history
483
562
  var mentionResponseEntry = {
@@ -504,6 +583,10 @@ function attachMateInteraction(ctx) {
504
583
  sendToSession(session.localId, { type: "mention_done", mateId: msg.mateId });
505
584
  send({ type: "mention_processing", mateId: msg.mateId, active: false });
506
585
 
586
+ if (isMate) {
587
+ notifyMentionResponse(session, msg.mateId, mateName, fullText);
588
+ }
589
+
507
590
  // Check if the mate wrote a debate brief during this turn
508
591
  ctx.checkForDmDebateBrief(session, msg.mateId, mateCtx);
509
592
 
@@ -512,6 +595,7 @@ function attachMateInteraction(ctx) {
512
595
  },
513
596
  onError: function (errMsg) {
514
597
  session._mentionInProgress = false;
598
+ session._mentionActiveMateId = null;
515
599
  // Clean up dead session
516
600
  if (session._mentionSessions && session._mentionSessions[msg.mateId]) {
517
601
  delete session._mentionSessions[msg.mateId];
@@ -560,6 +644,7 @@ function attachMateInteraction(ctx) {
560
644
 
561
645
  // Create new persistent mention session
562
646
  sdk.createMentionSession({
647
+ vendor: mate.vendor || null,
563
648
  claudeMd: claudeMd,
564
649
  initialContext: mentionContext,
565
650
  initialMessage: mentionFullInput,
@@ -614,6 +699,7 @@ function attachMateInteraction(ctx) {
614
699
  }
615
700
  }).catch(function (err) {
616
701
  session._mentionInProgress = false;
702
+ session._mentionActiveMateId = null;
617
703
  console.error("[mention] Failed to create session for mate " + msg.mateId + ":", err.message || err);
618
704
  sendToSession(session.localId, { type: "mention_error", mateId: msg.mateId, error: "Failed to create mention session." });
619
705
  });
@@ -60,6 +60,15 @@ var formatters = {
60
60
  body: data.preview || "Sent you a message",
61
61
  };
62
62
  },
63
+
64
+ mention_response: function (data) {
65
+ return {
66
+ type: "mention_response",
67
+ title: data.title || "Mention response ready",
68
+ body: data.preview || "",
69
+ meta: { avatarMateId: data.avatarMateId || null },
70
+ };
71
+ },
63
72
  };
64
73
 
65
74
  // ========================================================
@@ -1,6 +1,7 @@
1
1
  var fs = require("fs");
2
2
  var path = require("path");
3
3
  var { execFileSync } = require("child_process");
4
+ var { CODEX_DEFAULTS, getCodexConfig } = require("./codex-defaults");
4
5
 
5
6
  /**
6
7
  * Attach session management, config, project management, and mid-section
@@ -280,6 +281,7 @@ function attachSessions(ctx) {
280
281
  if (msg.id && sm.sessions.has(msg.id) && msg.title) {
281
282
  var s = sm.sessions.get(msg.id);
282
283
  s.title = String(msg.title).substring(0, 100);
284
+ s.titleManuallySet = true;
283
285
  sm.saveSessionFile(s);
284
286
  sm.broadcastSessionList();
285
287
  // Sync title to SDK session
@@ -519,18 +521,18 @@ function attachSessions(ctx) {
519
521
 
520
522
  // Codex-specific settings (stored on sessionManager, passed to adapter via adapterOptions)
521
523
  if (msg.type === "set_codex_approval") {
522
- sm.codexApproval = msg.approval || "on-failure";
523
- send({ type: "codex_config", approval: sm.codexApproval, sandbox: sm.codexSandbox || "workspace-write", webSearch: sm.codexWebSearch || "disabled" });
524
+ sm.codexApproval = msg.approval || CODEX_DEFAULTS.approval;
525
+ send(Object.assign({ type: "codex_config" }, getCodexConfig(sm)));
524
526
  return true;
525
527
  }
526
528
  if (msg.type === "set_codex_sandbox") {
527
- sm.codexSandbox = msg.sandbox || "workspace-write";
528
- send({ type: "codex_config", approval: sm.codexApproval || "on-failure", sandbox: sm.codexSandbox, webSearch: sm.codexWebSearch || "disabled" });
529
+ sm.codexSandbox = msg.sandbox || CODEX_DEFAULTS.sandbox;
530
+ send(Object.assign({ type: "codex_config" }, getCodexConfig(sm)));
529
531
  return true;
530
532
  }
531
533
  if (msg.type === "set_codex_websearch") {
532
- sm.codexWebSearch = msg.webSearch || "disabled";
533
- send({ type: "codex_config", approval: sm.codexApproval || "on-failure", sandbox: sm.codexSandbox || "workspace-write", webSearch: sm.codexWebSearch });
534
+ sm.codexWebSearch = msg.webSearch || CODEX_DEFAULTS.webSearch;
535
+ send(Object.assign({ type: "codex_config" }, getCodexConfig(sm)));
534
536
  return true;
535
537
  }
536
538
 
@@ -596,6 +598,10 @@ function attachSessions(ctx) {
596
598
  }
597
599
  session.history = session.history.slice(0, trimTo);
598
600
  session.messageUUIDs = session.messageUUIDs.slice(0, targetIdx);
601
+ // Reset digest checkpoint if it points past the trimmed history
602
+ if (typeof session._dmLastDigestedIndex === "number" && session._dmLastDigestedIndex > trimTo) {
603
+ session._dmLastDigestedIndex = trimTo;
604
+ }
599
605
  }
600
606
 
601
607
  // Notify adapter of conversation rollback (e.g. Codex thread/rollback)
package/lib/project.js CHANGED
@@ -615,15 +615,33 @@ function createProjectContext(opts) {
615
615
  var tm = createTerminalManager({ cwd: cwd, send: send, sendTo: sendTo });
616
616
  var nm = createNotesManager({ cwd: cwd, send: send, sendTo: sendTo });
617
617
 
618
- // Check for updates in background (admin only)
619
- fetchVersion(updateChannel).then(function (v) {
620
- if (v && isNewer(v, currentVersion)) {
621
- latestVersion = v;
622
- sendToAdmins({ type: "update_available", version: v });
623
- }
624
- }).catch(function (e) {
625
- console.error("[project] Background version check failed:", e.message || e);
626
- });
618
+ // Check for updates in background (admin only). The result is stored in
619
+ // latestVersion; broadcast is handled by the hourly scheduler below, so
620
+ // page refreshes don't re-trigger the banner.
621
+ function runVersionCheck(broadcast) {
622
+ fetchVersion(updateChannel).then(function (v) {
623
+ if (v && isNewer(v, currentVersion)) {
624
+ latestVersion = v;
625
+ if (broadcast) sendToAdmins({ type: "update_available", version: v });
626
+ }
627
+ }).catch(function (e) {
628
+ console.error("[project] Background version check failed:", e.message || e);
629
+ });
630
+ }
631
+ runVersionCheck(false);
632
+
633
+ // Push update_available on every hour boundary. Clients can dismiss the
634
+ // banner; the next hourly push acts as a fresh ping. This avoids needing
635
+ // any dismissed-state persistence.
636
+ function scheduleNextHourlyBroadcast() {
637
+ var now = Date.now();
638
+ var msUntilNextHour = 60 * 60 * 1000 - (now % (60 * 60 * 1000));
639
+ setTimeout(function tick() {
640
+ runVersionCheck(true);
641
+ setTimeout(tick, 60 * 60 * 1000);
642
+ }, msUntilNextHour);
643
+ }
644
+ scheduleNextHourlyBroadcast();
627
645
 
628
646
  // --- WS connection handler (delegated to project-connection.js) ---
629
647
  function handleConnection(ws, wsUser) {
@@ -726,6 +744,7 @@ function createProjectContext(opts) {
726
744
  delete session._mentionSessions[mateId];
727
745
  }
728
746
  session._mentionInProgress = false;
747
+ session._mentionActiveMateId = null;
729
748
  sendToSession(session.localId, { type: "mention_done", mateId: mateId, stopped: true });
730
749
  send({ type: "mention_processing", mateId: mateId, active: false });
731
750
  }
@@ -849,6 +868,7 @@ function createProjectContext(opts) {
849
868
  // so we use a lazy getter that resolves at call time.
850
869
  var _mateInteraction = attachMateInteraction({
851
870
  cwd: cwd,
871
+ slug: slug,
852
872
  sm: sm,
853
873
  sdk: sdk,
854
874
  send: send,
@@ -866,6 +886,7 @@ function createProjectContext(opts) {
866
886
  loadMateDigests: loadMateDigests,
867
887
  updateMemorySummary: updateMemorySummary,
868
888
  initMemorySummary: initMemorySummary,
889
+ getNotificationsModule: function () { return _notifications; },
869
890
  get checkForDmDebateBrief() { return checkForDmDebateBrief; },
870
891
  });
871
892
  var handleMention = _mateInteraction.handleMention;
package/lib/public/app.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  } from './modules/sidebar-projects.js';
13
13
  import {
14
14
  renderUserStrip, closeDmUserPicker, setCurrentDmUser,
15
- updateDmBadge, renderSidebarPresence
15
+ updateDmBadge, renderSidebarPresence, setMentionActive, clearAllMentionActive
16
16
  } from './modules/sidebar-mates.js';
17
17
  import {
18
18
  openMobileSheet, setMobileSheetMateData, refreshMobileChatSheet
@@ -309,6 +309,9 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
309
309
  currentBetas: [],
310
310
  currentThinking: "adaptive",
311
311
  currentThinkingBudget: 10000,
312
+ codexApproval: null,
313
+ codexSandbox: null,
314
+ codexWebSearch: null,
312
315
  ctxPopoverVisible: false,
313
316
  headerContextEl: null,
314
317
  // loop
@@ -386,7 +389,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
386
389
  openDm: function (userId) { openDm(userId); },
387
390
  openMateWizard: function () { requireClayMateInterview(function () { openMateWizard(); }); },
388
391
  openAddProjectModal: function () { openAddProjectModal(); },
389
- sendWs: function (msg) { if (ws && ws.readyState === 1) ws.send(JSON.stringify(msg)); },
392
+ sendWs: function (msg) { var _ws = _getWsRef(); if (_ws && _ws.readyState === 1) _ws.send(JSON.stringify(msg)); },
390
393
  onDmRemoveUser: function (userId) { var dr = Object.assign({}, store.get('dmRemovedUsers')); dr[userId] = true; store.set({ dmRemovedUsers: dr }); },
391
394
  getHistoryFrom: function () { return store.get('historyFrom'); },
392
395
  get permissions() { return store.get('permissions'); },
@@ -399,7 +402,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
399
402
  initMateKnowledge(wsGetter);
400
403
  initMateMemory(wsGetter, { onShow: function () { hideKnowledge(); hideNotes(); } });
401
404
  initMateWizard(
402
- function (msg) { if (ws && ws.readyState === 1) ws.send(JSON.stringify(msg)); },
405
+ function (msg) { var _ws = _getWsRef(); if (_ws && _ws.readyState === 1) _ws.send(JSON.stringify(msg)); },
403
406
  function (mate) { handleMateCreatedInApp(mate); }
404
407
  );
405
408
 
@@ -420,8 +423,9 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
420
423
  var stickyBtn = document.getElementById("sticky-notes-sidebar-btn");
421
424
  if (stickyBtn) stickyBtn.click();
422
425
  }
423
- if (ws && ws.readyState === 1) {
424
- ws.send(JSON.stringify({ type: "switch_session", id: id }));
426
+ var _ws = _getWsRef();
427
+ if (_ws && _ws.readyState === 1) {
428
+ _ws.send(JSON.stringify({ type: "switch_session", id: id }));
425
429
  }
426
430
  },
427
431
  openDm: function (userId) { openDm(userId); },
@@ -518,6 +522,15 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
518
522
  forceScrollToBottom();
519
523
  });
520
524
 
525
+ // Scroll to bottom when returning to the app (e.g. switching back from another
526
+ // Android app or tab). Without this the browser restores a stale scroll offset
527
+ // and leaves the user stranded mid-session instead of at the latest message.
528
+ document.addEventListener("visibilitychange", function () {
529
+ if (!document.hidden) {
530
+ scrollToBottom();
531
+ }
532
+ });
533
+
521
534
  // Fork session from a user message
522
535
  messagesEl.addEventListener("click", function(e) {
523
536
  var btn = e.target.closest(".msg-action-fork");
@@ -700,7 +713,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
700
713
  isDebateFloorMode: function () { return store.get('debateFloorMode'); },
701
714
  handleDebateFloorSend: function () { handleDebateFloorSend(); },
702
715
  isMateDm: function () { return store.get('dmMode') && store.get('dmTargetUser') && store.get('dmTargetUser').isMate; },
703
- getDmMateId: function () { return (store.get('dmMode') && store.get('dmTargetUser') && store.get('dmTargetUser').isMate) ? dmTargetUser.id : null; },
716
+ getDmMateId: function () { var _dmt = store.get('dmTargetUser'); return (store.get('dmMode') && _dmt && _dmt.isMate) ? _dmt.id : null; },
704
717
  getMateName: function () { return store.get('dmTargetUser') ? (store.get('dmTargetUser').displayName || "Mate") : "Mate"; },
705
718
  getMateAvatarUrl: function () { return document.body.dataset.mateAvatarUrl || ""; },
706
719
  showMatePreThinking: function () { showMatePreThinking(); },
@@ -726,7 +739,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
726
739
  // --- Debate module ---
727
740
  initDebate({
728
741
  get ws() { return _getWsRef(); },
729
- sendWs: function (obj) { if (ws && ws.readyState === 1) ws.send(JSON.stringify(obj)); },
742
+ sendWs: function (obj) { var _ws = _getWsRef(); if (_ws && _ws.readyState === 1) _ws.send(JSON.stringify(obj)); },
730
743
  messagesEl: messagesEl,
731
744
  addToMessages: function (el) { addToMessages(el); },
732
745
  scrollToBottom: scrollToBottom,
@@ -781,6 +794,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
781
794
  var _perms = store.get('permissions');
782
795
  if (_perms) {
783
796
  if (!_perms.terminal) {
797
+ var termBtn = document.getElementById("terminal-toggle-btn");
798
+ if (termBtn) termBtn.style.display = "none";
784
799
  var termSideBtn = document.getElementById("terminal-sidebar-btn");
785
800
  if (termSideBtn) termSideBtn.style.display = "none";
786
801
  }