clay-server 4.1.0-beta.15 → 4.1.0-beta.16

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.
@@ -346,7 +346,11 @@ function attachConnection(ctx) {
346
346
  var dcPresKey = ws._clayUser ? ws._clayUser.id : "_default";
347
347
  var dcExisting = userPresence.getPresence(slug, dcPresKey);
348
348
  var dcSession = sm.sessions.get(ws._clayActiveSession);
349
- userPresence.setPresence(slug, dcPresKey, userPresence.sessionIdForPersistence(dcSession), dcExisting ? dcExisting.mateDm : null);
349
+ var dcSessionId = userPresence.sessionIdForPersistence(dcSession);
350
+ var dcExistingSession = dcExisting && userPresence.findSession(sm.sessions, dcExisting.sessionId);
351
+ if (userPresence.shouldPersistDisconnectPresence(dcExisting, dcSessionId, dcExistingSession, dcSession)) {
352
+ userPresence.setPresence(slug, dcPresKey, dcSessionId, dcExisting ? dcExisting.mateDm : null);
353
+ }
350
354
  }
351
355
  tm.detachAll(ws);
352
356
  clients.delete(ws);
@@ -984,6 +984,8 @@ function attachSessions(ctx) {
984
984
  if (msg.id && sm.sessions.has(msg.id) && msg.title) {
985
985
  var s = sm.sessions.get(msg.id);
986
986
  s.title = String(msg.title).substring(0, 100);
987
+ s.titleProvisional = false;
988
+ s.titleAutoGenerated = false;
987
989
  s.titleManuallySet = true;
988
990
  sm.saveSessionFile(s);
989
991
  sm.notifySessionRenamed(s.localId);
@@ -479,6 +479,10 @@ function attachUserMessage(ctx) {
479
479
  function recordSessionTitle() {
480
480
  if (!session.title) {
481
481
  session.title = (msg.text || "Image").substring(0, 50);
482
+ // This title is an optimistic first-message label, not a semantic or
483
+ // explicit title. Persist its provenance so startQuery can preserve
484
+ // eligibility for model generation after this function returns.
485
+ session.titleProvisional = true;
482
486
  sm.saveSessionFile(session);
483
487
  sm.broadcastSessionList();
484
488
  // Sync auto-title to SDK
package/lib/project.js CHANGED
@@ -1598,7 +1598,7 @@ function createProjectContext(opts) {
1598
1598
  }
1599
1599
 
1600
1600
  // --- DM messages (delegated to server-level handler) ---
1601
- if (msg.type === "home_debate_question_response" || msg.type === "home_debate_control" || msg.type === "home_mate_creation_question_response" || msg.type === "default_ai_get" || msg.type === "default_ai_catalog_get" || msg.type === "default_ai_set" || msg.type === "default_vendor_get" || msg.type === "default_vendor_set" || msg.type === "cursor_sharing_get" || msg.type === "cursor_sharing_set") {
1601
+ if (msg.type === "home_debate_question_response" || msg.type === "home_debate_control" || msg.type === "home_mate_creation_question_response" || msg.type === "default_ai_get" || msg.type === "default_ai_catalog_get" || msg.type === "default_ai_set" || msg.type === "default_vendor_get" || msg.type === "default_vendor_set" || msg.type === "context_view_get" || msg.type === "context_view_set" || msg.type === "cursor_sharing_get" || msg.type === "cursor_sharing_set") {
1602
1602
  if (typeof opts.onDmMessage === "function") opts.onDmMessage(ws, msg, slug);
1603
1603
  return;
1604
1604
  }
package/lib/public/app.js CHANGED
@@ -287,6 +287,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
287
287
  currentSlug: currentSlug,
288
288
  activeProjectSlug: null,
289
289
  sessionActivatedProjectSlug: null,
290
+ sessionListProjectSlug: null,
291
+ splitGroupsProjectSlug: null,
290
292
  socketPath: null,
291
293
  pendingHomeProjectSlug: null,
292
294
  currentProjectOwnerId: null,
@@ -445,6 +447,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
445
447
  pendingShellCommandId: null,
446
448
  mateProjectSlug: null,
447
449
  myUserId: null,
450
+ contextViewPreferenceState: { mode: "off", canonicalMode: "off", preferencePresent: false, loading: false, saving: false, requestId: null, saveRequestId: null, pendingSaves: [], accountId: null, serverEpoch: null, rejectedEpoch: null, canonicalRevision: 0, error: "" },
451
+ contextViewOverride: null,
448
452
  isMultiUserMode: false,
449
453
  dmUnread: {},
450
454
  dmRemovedUsers: {},
@@ -17,7 +17,9 @@ import { resumeHomeChat } from './home-mate-chat.js';
17
17
  import { requestHomeSurfacePreference } from './home-surface.js';
18
18
  import { isHomeDebatesSurface } from './home-sub-surface.js';
19
19
  import { beginDefaultAiConnection, requestDefaultAi } from './default-ai.js';
20
+ import { beginContextViewConnection, requestContextView } from './context-view-preference.js';
20
21
  import { beginDefaultVendorConnection, requestDefaultVendor } from './default-vendor.js';
22
+ import { clearProjectSplitState } from './split-session-boundary.js';
21
23
 
22
24
  var reconnectTimer = null;
23
25
  var reconnectDelay = 1000;
@@ -234,7 +236,8 @@ export function connect() {
234
236
 
235
237
  var protocol = location.protocol === "https:" ? "wss:" : "ws:";
236
238
  var socketPath = store.get('wsPath');
237
- store.set({ socketPath: socketPath, activeProjectSlug: null, sessionActivatedProjectSlug: null });
239
+ if (store.get('socketPath') && store.get('socketPath') !== socketPath) clearProjectSplitState();
240
+ store.set({ socketPath: socketPath, activeProjectSlug: null, sessionActivatedProjectSlug: null, sessionListProjectSlug: null, splitGroupsProjectSlug: null });
238
241
  var newWs = new WebSocket(protocol + "//" + location.host + socketPath);
239
242
  setWs(newWs);
240
243
 
@@ -280,6 +283,8 @@ export function connect() {
280
283
  requestDefaultAi();
281
284
  beginDefaultVendorConnection();
282
285
  requestDefaultVendor();
286
+ beginContextViewConnection();
287
+ requestContextView();
283
288
  };
284
289
 
285
290
  newWs.onclose = function (e) {
@@ -20,6 +20,7 @@ import { renderMateSessionList, handleMateSearchResults, updateMateSidebarProfil
20
20
  import { openHomeChat, handleHomeMateHistory, handleHomeMateDelta, handleHomeMateSegment, handleHomeMateDone, handleHomeMateError, handleHomeMateSessionsState } from './home-mate-chat.js';
21
21
  import { handleHomeSurfaceState } from './home-surface.js';
22
22
  import { handleDefaultVendorMessage } from './default-vendor.js';
23
+ import { handleContextViewMessage } from './context-view-preference.js';
23
24
  import { handleHomeMateMemoryState, handleHomeMateKnowledgeState } from './home-mate-settings.js';
24
25
  import { renderKnowledgeList, handleKnowledgeContent } from './mate-knowledge.js';
25
26
  import { renderMemoryList } from './mate-memory.js';
@@ -64,6 +65,7 @@ import { setStatus } from './app-connection.js';
64
65
  import { handleWhatsNewState, handleWhatsNewSeenResult, setKnownEntries as setWhatsNewKnownEntries } from './whats-new.js';
65
66
  import { closeArticle as closeWhatsNewArticle } from './whats-new-article.js';
66
67
  import { resolvePaneSession, resolveSwitchedVendor } from './pane-session.js';
68
+ import { markProjectSessionListHydrated, applyProjectSplitGroups, clearProjectSplitState } from './split-session-boundary.js';
67
69
  import { selectDefaultVendorForBlankSession } from './vendor-selection.js';
68
70
  import { getModelInfoUpdate, modelEntryValue, modelEntryMatches, handleModelSelectionResult, requestVendorModels } from './model-picker.js';
69
71
  import { getModelEffortLevels, accumulateUsage, updateUsagePanel, accumulateContext, updateContextPanel, renderCtxPopover, updateStatusPanel, markContextUnavailable } from './app-panels.js';
@@ -99,6 +101,7 @@ export function processMessage(msg) {
99
101
  if (handleLoopInterviewMessage(msg)) return;
100
102
  if (handleScheduledTaskMessage(msg)) return;
101
103
  if (handleDefaultVendorMessage(msg)) return;
104
+ if (handleContextViewMessage(msg)) return;
102
105
  if (msg && msg.type === "schedule_message_result") {
103
106
  handleScheduleMessageResult(msg);
104
107
  return;
@@ -331,10 +334,13 @@ export function processMessage(msg) {
331
334
  // start receiving the new project's session_switched. The TUI
332
335
  // host lives on document.body (it's position: fixed), so it
333
336
  // survives project navigation unless we detach explicitly here.
337
+ if (msg.slug && store.get('activeProjectSlug') && store.get('activeProjectSlug') !== msg.slug) {
338
+ clearProjectSplitState();
339
+ }
334
340
  detachTuiView();
335
341
  store.set({ projectName: msg.project || msg.cwd, vendorInfo: msg.vendors || {} });
336
342
  if (msg.cwd) store.set({ cwd: msg.cwd });
337
- if (msg.slug) store.set({ currentSlug: msg.slug, activeProjectSlug: msg.slug });
343
+ if (msg.slug) store.set({ currentSlug: msg.slug, activeProjectSlug: msg.slug, sessionListProjectSlug: null, splitGroupsProjectSlug: null });
338
344
  try { var _is = store.snap(); localStorage.setItem("clay-project-name-" + (_is.currentSlug || "default"), _is.projectName); } catch (e) {}
339
345
  // In mate DM, keep title as mate name and re-apply mate color
340
346
  if (store.get('dmMode') && store.get('dmTargetUser') && store.get('dmTargetUser').isMate) {
@@ -691,6 +697,7 @@ export function processMessage(msg) {
691
697
  break;
692
698
 
693
699
  case "session_list":
700
+ markProjectSessionListHydrated();
694
701
  renderMateSessionList(msg.sessions || []);
695
702
  renderSessionList(msg.sessions || []);
696
703
  syncPaneTitles();
@@ -702,13 +709,14 @@ export function processMessage(msg) {
702
709
  }
703
710
  }
704
711
  handlePaletteSessionSwitch();
712
+ maybeRestoreSplitGroup();
705
713
  break;
706
714
 
707
715
  case "split_groups":
708
- store.set({ splitGroups: msg.groups || [] });
716
+ applyProjectSplitGroups(msg.groups || []);
709
717
  renderSessionList(null);
710
- maybeRestoreSplitGroup();
711
718
  syncPaneTitles();
719
+ maybeRestoreSplitGroup();
712
720
  break;
713
721
 
714
722
  case "split_group_result":
@@ -889,8 +897,8 @@ export function processMessage(msg) {
889
897
  }
890
898
  // Reload survival: a restored active session that is a split-group
891
899
  // member reopens its group (no-op when a split is already open).
892
- maybeRestoreSplitGroup();
893
900
  if (finishProjectSessionActivation()) hideHomeHub();
901
+ maybeRestoreSplitGroup();
894
902
  break;
895
903
 
896
904
  case "session_full_access_changed":
@@ -5,6 +5,7 @@ import { refreshIcons } from "./icons.js";
5
5
  import { escapeHtml, showToast } from "./utils.js";
6
6
  import { store } from './store.js';
7
7
  import { getWs } from './ws-ref.js';
8
+ import { getContextView as getStoredContextView, getEffectiveContextView, setContextView as setStoredContextView } from './context-view-preference.js';
8
9
  import { reportPaneContext } from './pane-bridge.js';
9
10
  import { setupModelPicker, renderModelPicker, requestVendorModels, prepareModelPickerOpen, modelDisplayName } from './model-picker.js';
10
11
 
@@ -14,6 +15,7 @@ var contextData = { contextWindow: 0, currentInput: null, maxOutputTokens: 0, mo
14
15
  var ctxPopoverEl = null;
15
16
  var ctxHoverTimer = null;
16
17
  var statusRefreshTimer = null;
18
+ var lastAppliedContextView = null;
17
19
 
18
20
  // --- DOM refs ---
19
21
  var configChipWrap = null;
@@ -374,6 +376,15 @@ export function initPanels() {
374
376
 
375
377
  // --- Reactive UI sync ---
376
378
  store.subscribe(function (state, prev) {
379
+ var effectiveContextView = getEffectiveContextView();
380
+ if (effectiveContextView !== lastAppliedContextView) {
381
+ lastAppliedContextView = effectiveContextView;
382
+ applyContextView(effectiveContextView);
383
+ }
384
+ if (state.contextViewPreferenceState && state.contextViewPreferenceState.error &&
385
+ state.contextViewPreferenceState.error !== (prev.contextViewPreferenceState && prev.contextViewPreferenceState.error)) {
386
+ showToast(state.contextViewPreferenceState.error, "error");
387
+ }
377
388
  // richContextUsage changed -> update popover + panel
378
389
  if (state.richContextUsage !== prev.richContextUsage) {
379
390
  if (state.richContextUsage) {
@@ -540,7 +551,6 @@ export function initPanels() {
540
551
  if (contextPanelClose) {
541
552
  contextPanelClose.addEventListener("click", function () {
542
553
  setContextView("off");
543
- applyContextView("off");
544
554
  });
545
555
  }
546
556
 
@@ -553,7 +563,8 @@ export function initPanels() {
553
563
  }
554
564
 
555
565
  // Restore context view on load
556
- applyContextView(getContextView());
566
+ lastAppliedContextView = getEffectiveContextView();
567
+ applyContextView(lastAppliedContextView);
557
568
  }
558
569
 
559
570
  // --- Config chip ---
@@ -889,11 +900,15 @@ export function accumulateContext(cost, usage, modelUsage, lastStreamInputTokens
889
900
 
890
901
  // contextView: "off" | "mini" | "panel"
891
902
  export function getContextView() {
892
- try { return localStorage.getItem("clay-context-view") || "off"; } catch (e) { return "off"; }
903
+ return getStoredContextView();
893
904
  }
894
905
 
895
906
  export function setContextView(v) {
896
- try { localStorage.setItem("clay-context-view", v); } catch (e) {}
907
+ if (store.get("paneMode") === true) {
908
+ store.set({ contextViewOverride: v });
909
+ return true;
910
+ }
911
+ return setStoredContextView(v);
897
912
  }
898
913
 
899
914
  export function applyContextView(view) {
@@ -917,28 +932,24 @@ export function markContextUnavailable() {
917
932
  export function resetContext() {
918
933
  resetContextData();
919
934
  // Keep view state, just reset data
920
- applyContextView(getContextView());
935
+ applyContextView(getEffectiveContextView());
921
936
  }
922
937
 
923
938
  export function minimizeContext() {
924
939
  setContextView("mini");
925
- applyContextView("mini");
926
940
  }
927
941
 
928
942
  export function expandContext() {
929
943
  setContextView("panel");
930
- applyContextView("panel");
931
944
  }
932
945
 
933
946
  export function toggleContextPanel() {
934
947
  if (!contextPanel) return;
935
- var view = getContextView();
948
+ var view = getEffectiveContextView();
936
949
  if (view === "panel") {
937
950
  setContextView("mini");
938
- applyContextView("mini");
939
951
  } else {
940
952
  setContextView("panel");
941
- applyContextView("panel");
942
953
  }
943
954
  }
944
955
 
@@ -39,6 +39,7 @@ import { closeProjectSettings } from './project-settings.js';
39
39
  import { chooseProjectAfterRemoval } from './project-removal-target.js';
40
40
  import { isProjectActivated, isProjectActivationPending, isProjectContextConnected, projectWsPath } from './project-activation.js';
41
41
  import { rememberHomePrimarySurface } from './home-surface.js';
42
+ import { clearProjectSplitState } from './split-session-boundary.js';
42
43
 
43
44
  // --- Module-owned state ---
44
45
  var cachedProjects = [];
@@ -447,6 +448,7 @@ export function switchProject(slug) {
447
448
  }
448
449
  if (homeVisible) store.set({ pendingHomeProjectSlug: slug });
449
450
  if (isProjectActivationPending(store.snap(), slug, ws)) return;
451
+ if (store.get('currentSlug') !== slug) clearProjectSplitState();
450
452
  resetFileBrowser();
451
453
  if (typeof closeScheduledTasks === "function") closeScheduledTasks();
452
454
  closeNotesBrowser();
@@ -0,0 +1,143 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+
4
+ var MODES = ["off", "mini", "panel"];
5
+ var sequence = 0;
6
+
7
+ function validMode(mode) { return MODES.indexOf(mode) !== -1; }
8
+ function nextRequestId(prefix) { sequence += 1; return prefix + Date.now() + "-" + sequence; }
9
+ function accountId() { return store.get("myUserId") || "default"; }
10
+ function initialState() {
11
+ return { mode: "off", canonicalMode: "off", preferencePresent: false, loading: false, saving: false, requestId: null, saveRequestId: null, pendingSaves: [], accountId: null, serverEpoch: null, rejectedEpoch: null, canonicalRevision: 0, error: "" };
12
+ }
13
+
14
+ function send(message) {
15
+ var ws = getWs();
16
+ if (!ws || ws.readyState !== 1) return false;
17
+ ws.send(JSON.stringify(message));
18
+ return true;
19
+ }
20
+
21
+ export function getContextView() {
22
+ var state = store.get("contextViewPreferenceState") || initialState();
23
+ return validMode(state.mode) ? state.mode : "off";
24
+ }
25
+
26
+ export function getEffectiveContextView() {
27
+ if (store.get("paneMode") !== true) return getContextView();
28
+ var override = store.get("contextViewOverride");
29
+ return override === null || override === undefined ? getContextView() : (validMode(override) ? override : getContextView());
30
+ }
31
+
32
+ export function beginContextViewConnection() {
33
+ var state = store.get("contextViewPreferenceState") || initialState();
34
+ store.set({ contextViewPreferenceState: Object.assign({}, state, {
35
+ mode: validMode(state.canonicalMode) ? state.canonicalMode : "off",
36
+ loading: false,
37
+ saving: false,
38
+ requestId: null,
39
+ saveRequestId: null,
40
+ pendingSaves: [],
41
+ rejectedEpoch: state.serverEpoch || null,
42
+ serverEpoch: null,
43
+ canonicalRevision: 0,
44
+ error: "",
45
+ }) });
46
+ }
47
+
48
+ export function requestContextView() {
49
+ var id = nextRequestId("context-view-");
50
+ var state = store.get("contextViewPreferenceState") || initialState();
51
+ store.set({ contextViewPreferenceState: Object.assign({}, state, { loading: true, requestId: id, error: "" }) });
52
+ if (!send({ type: "context_view_get", requestId: id })) {
53
+ store.set({ contextViewPreferenceState: Object.assign({}, store.get("contextViewPreferenceState") || initialState(), { loading: false, requestId: null, error: "Clay is offline. Reconnect and try again." }) });
54
+ return false;
55
+ }
56
+ return true;
57
+ }
58
+
59
+ export function setContextView(mode) {
60
+ if (!validMode(mode)) return false;
61
+ var id = nextRequestId("context-view-save-");
62
+ var state = store.get("contextViewPreferenceState") || initialState();
63
+ var pendingSaves = (state.pendingSaves || []).concat([{ requestId: id, mode: mode }]);
64
+ store.set({ contextViewPreferenceState: Object.assign({}, state, { mode: mode, saving: true, saveRequestId: id, pendingSaves: pendingSaves, error: "" }) });
65
+ if (!send({ type: "context_view_set", requestId: id, mode: mode })) {
66
+ var reverted = store.get("contextViewPreferenceState") || initialState();
67
+ pendingSaves = (reverted.pendingSaves || []).filter(function (item) { return item.requestId !== id; });
68
+ var pendingMode = pendingSaves.length ? pendingSaves[pendingSaves.length - 1].mode : (reverted.canonicalMode || "off");
69
+ store.set({ contextViewPreferenceState: Object.assign({}, reverted, { mode: pendingMode, saving: pendingSaves.length > 0, saveRequestId: pendingSaves.length ? pendingSaves[pendingSaves.length - 1].requestId : null, pendingSaves: pendingSaves, error: "Clay is offline. Reconnect and try again." }) });
70
+ return false;
71
+ }
72
+ return true;
73
+ }
74
+
75
+ export function handleContextViewMessage(msg) {
76
+ if (!msg || msg.type !== "context_view_state") return false;
77
+ var state = store.get("contextViewPreferenceState") || initialState();
78
+ var currentAccount = accountId();
79
+ var isGetReply = !!msg.requestId && msg.requestId === state.requestId;
80
+ var pendingSaves = state.pendingSaves || [];
81
+ var saveIndex = -1;
82
+ for (var i = 0; i < pendingSaves.length; i++) if (pendingSaves[i].requestId === msg.requestId) saveIndex = i;
83
+ var isSaveReply = !!msg.requestId && saveIndex !== -1;
84
+ var isOlderSaveRevision = isSaveReply && typeof msg.canonicalRevision === "number" && msg.canonicalRevision < (state.canonicalRevision || 0);
85
+ var isCorrelatedAccountFailure = (isGetReply || isSaveReply) && msg.accountId === null && msg.accountAvailable === false;
86
+ if ((!isCorrelatedAccountFailure && msg.accountId !== currentAccount) || (msg.accountId === null && !isCorrelatedAccountFailure)) return true;
87
+ if (msg.serverEpoch && state.serverEpoch && msg.serverEpoch !== state.serverEpoch && (!msg.requestId || (!isGetReply && !isSaveReply))) return true;
88
+ if (msg.serverEpoch && state.rejectedEpoch && msg.serverEpoch === state.rejectedEpoch && (!isGetReply && !isSaveReply)) return true;
89
+ if (msg.canonicalRevision !== undefined && msg.canonicalRevision < (state.canonicalRevision || 0) && msg.serverEpoch === state.serverEpoch && !isSaveReply) return true;
90
+ if (msg.requestId && !isGetReply && !isSaveReply) return true;
91
+ if (msg.requestId && !isSaveReply && msg.ready === false && state.saving) return true;
92
+ var next = Object.assign({}, state, {
93
+ accountId: msg.accountId || currentAccount,
94
+ serverEpoch: msg.serverEpoch || state.serverEpoch || null,
95
+ rejectedEpoch: isGetReply || isSaveReply ? null : state.rejectedEpoch || null,
96
+ canonicalRevision: isOlderSaveRevision ? (state.canonicalRevision || 0) : (typeof msg.canonicalRevision === "number" ? msg.canonicalRevision : state.canonicalRevision || 0),
97
+ loading: isGetReply ? false : state.loading,
98
+ saving: state.saving,
99
+ requestId: isGetReply ? null : state.requestId,
100
+ saveRequestId: state.saveRequestId,
101
+ error: msg.error || "",
102
+ });
103
+ if (Object.prototype.hasOwnProperty.call(msg, "mode") && validMode(msg.mode) && msg.ready !== false && !isCorrelatedAccountFailure) {
104
+ if (isSaveReply) {
105
+ if (!isOlderSaveRevision && msg.ready !== false) {
106
+ pendingSaves = pendingSaves.slice(saveIndex + 1);
107
+ } else {
108
+ pendingSaves = pendingSaves.filter(function (item) { return item.requestId !== msg.requestId; });
109
+ }
110
+ }
111
+ if (!isOlderSaveRevision) {
112
+ next.canonicalMode = msg.mode;
113
+ next.preferencePresent = msg.preferencePresent === true;
114
+ }
115
+ if (pendingSaves.length) {
116
+ next.mode = pendingSaves[pendingSaves.length - 1].mode;
117
+ next.saving = true;
118
+ next.saveRequestId = pendingSaves[pendingSaves.length - 1].requestId;
119
+ } else {
120
+ next.mode = next.canonicalMode;
121
+ next.saving = false;
122
+ next.saveRequestId = null;
123
+ }
124
+ next.pendingSaves = pendingSaves;
125
+ } else if (isSaveReply && (msg.ready === false || isCorrelatedAccountFailure)) {
126
+ pendingSaves = pendingSaves.filter(function (item) { return item.requestId !== msg.requestId; });
127
+ next.pendingSaves = pendingSaves;
128
+ next.mode = pendingSaves.length ? pendingSaves[pendingSaves.length - 1].mode : (state.canonicalMode || "off");
129
+ next.saving = pendingSaves.length > 0;
130
+ next.saveRequestId = pendingSaves.length ? pendingSaves[pendingSaves.length - 1].requestId : null;
131
+ }
132
+ store.set({ contextViewPreferenceState: next });
133
+ return true;
134
+ }
135
+
136
+ store.subscribe(function (state, previous) {
137
+ if (state.myUserId !== previous.myUserId || state.currentSlug !== previous.currentSlug) {
138
+ store.set({ contextViewPreferenceState: Object.assign({}, initialState(), { accountId: accountId() }), contextViewOverride: null });
139
+ if (state.connected) requestContextView();
140
+ }
141
+ });
142
+
143
+ export { validMode };
@@ -4,11 +4,9 @@
4
4
  // Same-origin messages only, both directions.
5
5
 
6
6
  import { store } from './store.js';
7
- import { applyContextView, getContextView } from './app-panels.js';
7
+ import { getEffectiveContextView } from './context-view-preference.js';
8
8
  import { forceExternalLinkToNewTab } from './pane-links.js';
9
9
 
10
- var panelOpen = false;
11
-
12
10
  export function reportPaneContext(data) {
13
11
  if (!store.get('paneMode') || window.parent === window) return;
14
12
  window.parent.postMessage({
@@ -38,11 +36,11 @@ export function forwardPaneMarkdownPresentation(message) {
38
36
  return true;
39
37
  }
40
38
 
41
- // Toggle without setContextView: panes share localStorage with the main app,
42
- // so persisting here would silently rewrite the user's main-view preference.
39
+ // Toggle without setContextView: panes use a transient override and must not
40
+ // rewrite the user's server-persisted main-view preference.
43
41
  function togglePaneContextPanel() {
44
- panelOpen = !panelOpen;
45
- applyContextView(panelOpen ? "panel" : "off");
42
+ var effectiveView = getEffectiveContextView();
43
+ store.set({ contextViewOverride: effectiveView === "panel" ? "off" : "panel" });
46
44
  }
47
45
 
48
46
  function preparePaneLink(event) {
@@ -55,7 +53,9 @@ function preparePaneLink(event) {
55
53
 
56
54
  export function initPaneBridge() {
57
55
  if (!store.get('paneMode')) return;
58
- panelOpen = getContextView() === "panel";
56
+ // Hydration may still replace the persisted mode. Leave the override null
57
+ // so the first header toggle derives from the actual effective view.
58
+ store.set({ contextViewOverride: null });
59
59
  document.addEventListener("click", preparePaneLink, true);
60
60
  document.addEventListener("auxclick", preparePaneLink, true);
61
61
  window.addEventListener("message", function (event) {
@@ -43,3 +43,52 @@ export function isProjectContextConnected(state, slug, socket) {
43
43
  && state.connected === true
44
44
  && state.activeProjectSlug === slug;
45
45
  }
46
+
47
+ export function isProjectSessionReady(state, socket) {
48
+ var target = projectWsPath(state.currentSlug);
49
+ return !!socket
50
+ && socket.readyState === 1
51
+ && state.connected === true
52
+ && !!state.currentSlug
53
+ && state.wsPath === target
54
+ && state.socketPath === target
55
+ && state.activeProjectSlug === state.currentSlug
56
+ && state.sessionActivatedProjectSlug === state.currentSlug
57
+ && state.sessionListProjectSlug === state.currentSlug
58
+ && state.splitGroupsProjectSlug === state.currentSlug;
59
+ }
60
+
61
+ export function splitPanesMatchProject(split, slug) {
62
+ if (!split || !Array.isArray(split.panes) || split.panes.length !== 2 || !slug) return false;
63
+ return split.panes[0].slug === slug && split.panes[1].slug === slug;
64
+ }
65
+
66
+ export function hasCurrentSplitGroup(split, groups) {
67
+ if (!split || !split.groupId) return true;
68
+ var members = split.panes && split.panes.map(function (pane) { return pane.sessionId; });
69
+ if (!members || members.length !== 2) return false;
70
+ var available = Array.isArray(groups) ? groups : [];
71
+ for (var i = 0; i < available.length; i++) {
72
+ var group = available[i];
73
+ if (group.id !== split.groupId || !Array.isArray(group.members) || group.members.length !== 2) continue;
74
+ if (group.members.indexOf(members[0]) !== -1 && group.members.indexOf(members[1]) !== -1) return true;
75
+ }
76
+ return false;
77
+ }
78
+
79
+ export function reconcileRestoredSplit(split, groups) {
80
+ if (!split || !split.groupId) return { action: "none" };
81
+ var available = Array.isArray(groups) ? groups : [];
82
+ for (var i = 0; i < available.length; i++) {
83
+ var group = available[i];
84
+ if (group.id !== split.groupId) continue;
85
+ var oldMembers = split.panes && split.panes.map(function (pane) { return pane.sessionId; });
86
+ var members = Array.isArray(group.members) ? group.members : [];
87
+ if (oldMembers && oldMembers.length === 2 && members.length === 2
88
+ && members.indexOf(oldMembers[0]) !== -1 && members.indexOf(oldMembers[1]) !== -1) {
89
+ return { action: "keep", group: group };
90
+ }
91
+ return { action: "rebuild", group: group };
92
+ }
93
+ return { action: "clear" };
94
+ }
@@ -0,0 +1,28 @@
1
+ import { store } from './store.js';
2
+ import { isProjectSessionReady } from './project-activation.js';
3
+
4
+ export function isCurrentProjectSessionReady(state, socket) {
5
+ return isProjectSessionReady(state, socket);
6
+ }
7
+
8
+ export function clearProjectSplitState() {
9
+ store.set({
10
+ splitPanes: null,
11
+ splitGroups: [],
12
+ splitDelegations: {},
13
+ splitGroupsProjectSlug: null,
14
+ sessionListProjectSlug: null,
15
+ activeSessionId: null,
16
+ sessionActivatedProjectSlug: null,
17
+ });
18
+ }
19
+
20
+ export function markProjectSessionListHydrated() {
21
+ var slug = store.get('activeProjectSlug');
22
+ if (slug) store.set({ sessionListProjectSlug: slug });
23
+ }
24
+
25
+ export function applyProjectSplitGroups(groups) {
26
+ var slug = store.get('activeProjectSlug');
27
+ store.set({ splitGroups: groups || [], splitGroupsProjectSlug: slug || null });
28
+ }
@@ -12,6 +12,8 @@ import { showConfirm } from './app-misc.js';
12
12
  import { syncPairChrome } from './split-pair-ui.js';
13
13
  import { presentMarkdownEdit } from './filebrowser.js';
14
14
  import { autoStartLoginIfNeeded } from './vendor-login.js';
15
+ import { isCurrentProjectSessionReady } from './split-session-boundary.js';
16
+ import { splitPanesMatchProject, hasCurrentSplitGroup, reconcileRestoredSplit } from './project-activation.js';
15
17
 
16
18
  var host = null;
17
19
  var nativeApp = null;
@@ -85,10 +87,11 @@ function showDropOverlay() {
85
87
  dropOverlay.classList.add("visible");
86
88
  }
87
89
 
88
- function switchNativeSession(sessionId) {
90
+ function switchNativeSession(sessionId, expectedSlug) {
89
91
  store.set({ splitPanes: null });
90
92
  var ws = getWs();
91
- if (ws && ws.readyState === 1) {
93
+ var state = store.snap();
94
+ if ((!expectedSlug || expectedSlug === state.currentSlug) && isCurrentProjectSessionReady(state, ws)) {
92
95
  ws.send(JSON.stringify({ type: "switch_session", id: sessionId }));
93
96
  }
94
97
  }
@@ -99,7 +102,7 @@ function closePane(index) {
99
102
  if (split.groupId && getWs() && getWs().readyState === 1) {
100
103
  getWs().send(JSON.stringify({ type: "split_group_dissolve", id: split.groupId }));
101
104
  }
102
- switchNativeSession(split.panes[index === 0 ? 1 : 0].sessionId);
105
+ switchNativeSession(split.panes[index === 0 ? 1 : 0].sessionId, split.panes[index === 0 ? 1 : 0].slug);
103
106
  }
104
107
 
105
108
  function startPaneRename(header, titleEl, pane) {
@@ -372,7 +375,7 @@ export function openGroup(group) {
372
375
  var activeId = store.get('activeSessionId');
373
376
  if (activeId !== group.members[0] && activeId !== group.members[1]) {
374
377
  var ws = getWs();
375
- if (ws && ws.readyState === 1) {
378
+ if (isCurrentProjectSessionReady(store.snap(), ws)) {
376
379
  ws.send(JSON.stringify({ type: "switch_session", id: group.members[0] }));
377
380
  }
378
381
  }
@@ -384,7 +387,33 @@ export function openGroup(group) {
384
387
  // group member, reopen that group. Safe to call often -- no-ops unless a
385
388
  // group member is active natively with no split open.
386
389
  export function maybeRestoreSplitGroup() {
387
- if (store.get('paneMode') || store.get('splitPanes')) return;
390
+ var state = store.snap();
391
+ if (!isCurrentProjectSessionReady(state, getWs())) return;
392
+ var split = state.splitPanes;
393
+ if (split && !splitPanesMatchProject(split, state.currentSlug)) {
394
+ store.set({ splitPanes: null });
395
+ return;
396
+ }
397
+ if (state.paneMode) return;
398
+ if (split) {
399
+ var reconciliation = reconcileRestoredSplit(split, state.splitGroups);
400
+ if (reconciliation.action === "clear") {
401
+ store.set({ splitPanes: null });
402
+ return;
403
+ }
404
+ if (reconciliation.action === "rebuild") {
405
+ var rebuilt = reconciliation.group;
406
+ if (!sessionById(rebuilt.members[0]) || !sessionById(rebuilt.members[1])) {
407
+ store.set({ splitPanes: null });
408
+ return;
409
+ }
410
+ store.set({ splitPanes: {
411
+ groupId: rebuilt.id,
412
+ panes: [paneForSession(rebuilt.members[0]), paneForSession(rebuilt.members[1])],
413
+ } });
414
+ }
415
+ return;
416
+ }
388
417
  var activeId = store.get('activeSessionId');
389
418
  if (!activeId) return;
390
419
  var groups = store.get('splitGroups') || [];
@@ -505,12 +534,16 @@ export function initSplitView() {
505
534
  }
506
535
  }
507
536
  if (state.splitGroups !== prev.splitGroups) {
537
+ if (!isCurrentProjectSessionReady(state, getWs())) return;
508
538
  var split = state.splitPanes;
509
539
  if (!split || !split.panes) return;
540
+ if (!splitPanesMatchProject(split, state.currentSlug)) {
541
+ store.set({ splitPanes: null });
542
+ return;
543
+ }
510
544
  syncPairChrome(host, split);
511
545
  if (split.groupId) {
512
- var stillExists = state.splitGroups.some(function (group) { return group.id === split.groupId; });
513
- if (!stillExists) switchNativeSession(split.panes[0].sessionId);
546
+ if (!hasCurrentSplitGroup(split, state.splitGroups)) switchNativeSession(split.panes[0].sessionId, state.currentSlug);
514
547
  return;
515
548
  }
516
549
  var ids = [split.panes[0].sessionId, split.panes[1].sessionId];
package/lib/sdk-bridge.js CHANGED
@@ -13,6 +13,7 @@ var { attachMessageProcessor } = require("./sdk-message-processor");
13
13
  var homeDebateToolPolicy = require("./home-debate-tool-policy");
14
14
  var yoke = require("./yoke");
15
15
  var sessionTitlePolicy = require("./session-title-policy");
16
+ var sessionTitleGenerator = require("./session-title-generator");
16
17
  var loopInterview = require("./project-loop-interview");
17
18
 
18
19
  // Extract serializable tool descriptors from MCP server instances.
@@ -350,49 +351,13 @@ function createSDKBridge(opts) {
350
351
  return owner && owner.linuxUser ? owner.linuxUser : null;
351
352
  }
352
353
 
353
- // Auto-generate a session title via YOKE adapter.generateTitle().
354
- // Triggered by sdk-message-processor after AUTO_TITLE_TURN_THRESHOLD turns.
355
- function autoGenerateTitle(session) {
356
- var sessionAdapter = getAdapterForSession(session);
357
- if (typeof sessionAdapter.generateTitle !== "function") {
358
- console.log("[auto-title] adapter.generateTitle not available for vendor=" + sessionAdapter.vendor);
359
- return;
360
- }
361
- var userMessages = [];
362
- for (var i = 0; i < session.history.length; i++) {
363
- var entry = session.history[i];
364
- if (entry.type === "user_message" && entry.text) {
365
- userMessages.push(entry.text.substring(0, 200));
366
- if (userMessages.length >= 5) break;
367
- }
368
- }
369
- if (userMessages.length === 0) {
370
- console.log("[auto-title] No user messages found in session " + session.localId);
371
- return;
372
- }
373
- console.log("[auto-title] Calling adapter.generateTitle with " + userMessages.length + " messages for session " + session.localId);
374
-
375
- sessionAdapter.generateTitle(userMessages, {
376
- cwd: cwd,
377
- linuxUser: getSessionLinuxUser(session) || undefined,
378
- env: getRuntimeEnv(session),
379
- }).then(function(title) {
380
- if (!title || title.length < 2) return;
381
- title = title.substring(0, 100);
382
- if (!session.titleManuallySet) {
383
- session.title = title;
384
- session.titleAutoGenerated = true;
385
- sm.saveSessionFile(session);
386
- sm.broadcastSessionList();
387
- if (session.cliSessionId && typeof adapter.renameSession === "function") {
388
- adapter.renameSession(session.cliSessionId, title, { dir: cwd }).catch(function () {});
389
- }
390
- console.log("[auto-title] Generated title for session " + session.localId + ": " + title);
391
- }
392
- }).catch(function(e) {
393
- console.error("[auto-title] Failed:", e.message || e);
394
- });
395
- }
354
+ var titleGenerator = sessionTitleGenerator.attachSessionTitleGenerator({
355
+ sm: sm,
356
+ cwd: cwd,
357
+ getAdapter: getAdapterForSession,
358
+ getLinuxUser: getSessionLinuxUser,
359
+ getRuntimeEnv: getRuntimeEnv,
360
+ });
396
361
 
397
362
  var msgProcessor = attachMessageProcessor({
398
363
  sm: sm,
@@ -409,7 +374,7 @@ function createSDKBridge(opts) {
409
374
  getSessionLinuxUser: getSessionLinuxUser,
410
375
  onProcessingChanged: onProcessingChanged,
411
376
  onTurnDone: onTurnDone,
412
- onAutoTitle: function (session) { autoGenerateTitle(session); },
377
+ onAutoTitle: function (session) { titleGenerator.generate(session); },
413
378
  opts: opts,
414
379
  discoverSkillDirs: discoverSkillDirs,
415
380
  mergeSkills: mergeSkills,
@@ -1876,10 +1841,12 @@ function createSDKBridge(opts) {
1876
1841
  // project threshold); seeded titles remain fixed.
1877
1842
  if (initialTitle && !session.title) {
1878
1843
  session.title = initialTitle;
1844
+ session.titleProvisional = initialTitleProvisional;
1879
1845
  session.titleAutoGenerated = !initialTitleProvisional;
1880
1846
  sm.saveSessionFile(session);
1881
1847
  sm.broadcastSessionList();
1882
1848
  } else if (initialTitle && session.title === initialTitle) {
1849
+ session.titleProvisional = initialTitleProvisional;
1883
1850
  session.titleAutoGenerated = !initialTitleProvisional;
1884
1851
  sm.saveSessionFile(session);
1885
1852
  }
@@ -661,11 +661,13 @@ function attachMessageProcessor(ctx) {
661
661
  session.streamedText = false;
662
662
  sm.broadcastSessionList();
663
663
 
664
- // Auto-generate title after N turns (skip if loop or already auto-generated)
665
- if (session.turnCount === sessionTitlePolicy.generationTurn(isMate)
664
+ // Auto-generate after the first completed substantive turn. The title
665
+ // coordinator handles retries while generation is pending or failed.
666
+ if (session.turnCount >= sessionTitlePolicy.generationTurn(isMate)
666
667
  && !session.titleAutoGenerated
667
668
  && !session.titleManuallySet
668
669
  && !session.loop
670
+ && !(session.sessionProvenance && session.sessionProvenance.kind === "worker")
669
671
  && onAutoTitle) {
670
672
  try { onAutoTitle(session); } catch (e) {
671
673
  console.error("[auto-title] onAutoTitle threw:", e.message || e);
@@ -0,0 +1,53 @@
1
+ // Authenticated user-wide context display preference service.
2
+
3
+ function attachContextViewService(deps) {
4
+ var users = deps.users;
5
+ var forEachAppClient = deps.forEachAppClient;
6
+ var revisions = {};
7
+ var epoch = deps.serverEpoch || require("crypto").randomBytes(12).toString("hex");
8
+
9
+ function actor(ws) {
10
+ if (users.isMultiUser() !== true) return { userId: "default", user: true };
11
+ var claimed = ws && ws._clayUser && ws._clayUser.id;
12
+ var user = claimed ? users.findUserById(claimed) : null;
13
+ return user ? { userId: user.id, user: user } : { userId: null, user: null };
14
+ }
15
+ function sameActor(ws, userId) {
16
+ var current = actor(ws);
17
+ return current.user && current.userId === userId;
18
+ }
19
+ function send(ws, message) {
20
+ if (ws && ws.readyState === 1) ws.send(JSON.stringify(Object.assign({ serverEpoch: epoch }, message)));
21
+ }
22
+ function revision(userId) { return revisions[userId] || 0; }
23
+ function broadcast(userId, message, except) {
24
+ forEachAppClient(function (client) { if (client === except || !sameActor(client, userId)) return; send(client, message); });
25
+ }
26
+ function handleMessage(ws, msg) {
27
+ if (!msg || ["context_view_get", "context_view_set"].indexOf(msg.type) === -1) return false;
28
+ var requestId = typeof msg.requestId === "string" ? msg.requestId.slice(0, 200) : null;
29
+ var who = actor(ws);
30
+ if (!who.user) {
31
+ send(ws, { type: "context_view_state", requestId: requestId, accountAvailable: false, accountId: null, mode: null, error: "Your account is no longer available." });
32
+ return true;
33
+ }
34
+ if (msg.type === "context_view_get") {
35
+ var current = users.getContextViewPreference(who.userId);
36
+ send(ws, { type: "context_view_state", requestId: requestId, accountAvailable: true, accountId: who.userId, mode: current.mode, preferencePresent: current.present, canonicalRevision: revision(who.userId), ready: !current.error, error: current.error || "" });
37
+ return true;
38
+ }
39
+ var saved = users.setContextViewPreference(who.userId, msg.mode);
40
+ if (!saved.ok) {
41
+ send(ws, { type: "context_view_state", requestId: requestId, accountAvailable: true, accountId: who.userId, canonicalRevision: revision(who.userId), ready: false, error: saved.error });
42
+ return true;
43
+ }
44
+ revisions[who.userId] = revision(who.userId) + 1;
45
+ var state = { type: "context_view_state", requestId: null, accountAvailable: true, accountId: who.userId, mode: msg.mode, preferencePresent: true, canonicalRevision: revisions[who.userId], ready: true, error: "" };
46
+ broadcast(who.userId, state, ws);
47
+ send(ws, Object.assign({}, state, { requestId: requestId }));
48
+ return true;
49
+ }
50
+ return { handleMessage: handleMessage };
51
+ }
52
+
53
+ module.exports = { attachContextViewService: attachContextViewService };
package/lib/server.js CHANGED
@@ -18,6 +18,7 @@ var shouldSuppressHomeResponse = require("./home-response-notifications").should
18
18
  var serverHomePreferences = require("./server-home-preferences");
19
19
  var serverDefaultAi = require("./server-default-ai");
20
20
  var serverDefaultVendor = require("./server-default-vendor");
21
+ var serverContextView = require("./server-context-view");
21
22
  var serverCursorSharing = require("./server-cursor-sharing");
22
23
  var serverTools = require("./server-tools");
23
24
  var serverAdmin = require("./server-admin");
@@ -1069,6 +1070,7 @@ function createServer(opts) {
1069
1070
  var homePreferencesHandler = null;
1070
1071
  var defaultAiService = null;
1071
1072
  var defaultVendorService = null;
1073
+ var contextViewService = null;
1072
1074
  var cursorSharingService = null;
1073
1075
 
1074
1076
  // Slug-less /ws handler: lets a user with no projects yet load the regular
@@ -1555,6 +1557,7 @@ function createServer(opts) {
1555
1557
 
1556
1558
  defaultAiService = serverDefaultAi.attachDefaultAiService({ users: users, homeChatHandler: homeChatHandler, forEachAppClient: forEachAppClient });
1557
1559
  defaultVendorService = serverDefaultVendor.attachDefaultVendorService({ users: users, homeChatHandler: homeChatHandler, forEachAppClient: forEachAppClient });
1560
+ contextViewService = serverContextView.attachContextViewService({ users: users, forEachAppClient: forEachAppClient });
1558
1561
  cursorSharingService = serverCursorSharing.attachCursorSharingService({ users: users, forEachAppClient: forEachAppClient });
1559
1562
 
1560
1563
  // --- User-installed home tools handler ---
@@ -1584,6 +1587,7 @@ function createServer(opts) {
1584
1587
  function handleDmMessage(ws, msg, routedProjectSlug) {
1585
1588
  if (cursorSharingService && cursorSharingService.handleMessage(ws, msg)) return true;
1586
1589
  if (defaultVendorService && defaultVendorService.handleMessage(ws, msg, routedProjectSlug)) return true;
1590
+ if (contextViewService && contextViewService.handleMessage(ws, msg)) return true;
1587
1591
  if (defaultAiService && defaultAiService.handleMessage(ws, msg)) return true;
1588
1592
  if (assignmentService.handleMessage(ws, msg, routedProjectSlug)) return true;
1589
1593
  if (dmHandler.handleMessage(ws, msg)) return true;
@@ -0,0 +1,89 @@
1
+ // Coordinates bounded semantic title generation for completed conversations.
2
+
3
+ var MAX_ATTEMPTS = 3;
4
+
5
+ function isWorker(session) {
6
+ return !!(session && session.sessionProvenance && session.sessionProvenance.kind === "worker");
7
+ }
8
+
9
+ function hasUserMessage(session) {
10
+ var history = session && Array.isArray(session.history) ? session.history : [];
11
+ for (var i = 0; i < history.length; i++) {
12
+ if (history[i] && history[i].type === "user_message" && typeof history[i].text === "string" && history[i].text.trim()) return true;
13
+ }
14
+ return false;
15
+ }
16
+
17
+ function attachSessionTitleGenerator(ctx) {
18
+ var sm = ctx.sm;
19
+ var getAdapter = ctx.getAdapter;
20
+ var getLinuxUser = ctx.getLinuxUser || function () { return null; };
21
+ var getRuntimeEnv = ctx.getRuntimeEnv || function () { return process.env; };
22
+ var cwd = ctx.cwd;
23
+
24
+ function isLive(session) {
25
+ if (!session || session.destroying || isWorker(session)) return false;
26
+ if (!sm || !sm.sessions || typeof sm.sessions.get !== "function") return true;
27
+ return sm.sessions.get(session.localId) === session;
28
+ }
29
+
30
+ function messagesFor(session) {
31
+ var result = [];
32
+ var history = Array.isArray(session.history) ? session.history : [];
33
+ for (var i = 0; i < history.length; i++) {
34
+ var entry = history[i];
35
+ if (entry && entry.type === "user_message" && entry.text) {
36
+ result.push(entry.text.substring(0, 200));
37
+ if (result.length >= 5) break;
38
+ }
39
+ }
40
+ return result;
41
+ }
42
+
43
+ function generate(session) {
44
+ if (!isLive(session) || session.titleManuallySet || session.titleAutoGenerated || !hasUserMessage(session)) return false;
45
+ if (session._autoTitleInFlight || (session._autoTitleAttempts || 0) >= MAX_ATTEMPTS) return false;
46
+ var sessionAdapter = getAdapter(session);
47
+ if (!sessionAdapter || typeof sessionAdapter.generateTitle !== "function") return false;
48
+ var messages = messagesFor(session);
49
+ if (!messages.length) return false;
50
+
51
+ session._autoTitleAttempts = (session._autoTitleAttempts || 0) + 1;
52
+ session._autoTitleInFlight = true;
53
+ var requestTitle = session.title;
54
+ return Promise.resolve().then(function () {
55
+ return sessionAdapter.generateTitle(messages, {
56
+ cwd: cwd,
57
+ linuxUser: getLinuxUser(session) || undefined,
58
+ env: getRuntimeEnv(session),
59
+ });
60
+ }).then(function (title) {
61
+ title = typeof title === "string" ? title.trim() : "";
62
+ if (title.length < 2) return false;
63
+ title = title.substring(0, 100);
64
+ if (!isLive(session) || session.titleManuallySet || session.titleAutoGenerated || session.title !== requestTitle) return false;
65
+ session.title = title;
66
+ session.titleProvisional = false;
67
+ session.titleAutoGenerated = true;
68
+ sm.saveSessionFile(session);
69
+ sm.broadcastSessionList();
70
+ if (session.cliSessionId && typeof sessionAdapter.renameSession === "function") {
71
+ return Promise.resolve(sessionAdapter.renameSession(session.cliSessionId, title, { dir: cwd })).then(function () { return true; });
72
+ }
73
+ return true;
74
+ }).catch(function (e) {
75
+ console.error("[auto-title] Failed:", e.message || e);
76
+ return false;
77
+ }).then(function (result) {
78
+ session._autoTitleInFlight = false;
79
+ return result;
80
+ }, function (e) {
81
+ session._autoTitleInFlight = false;
82
+ throw e;
83
+ });
84
+ }
85
+
86
+ return { generate: generate, maxAttempts: MAX_ATTEMPTS };
87
+ }
88
+
89
+ module.exports = { attachSessionTitleGenerator: attachSessionTitleGenerator };
@@ -2,7 +2,7 @@
2
2
  // Initial titles are immediate, provisional UI labels. Model-generated titles
3
3
  // replace them after enough conversation exists to describe the actual topic.
4
4
 
5
- var DEFAULT_GENERATION_TURN = 2;
5
+ var DEFAULT_GENERATION_TURN = 1;
6
6
  var HOME_GENERATION_TURN = 1;
7
7
  var MAX_PROVISIONAL_CHARS = 60;
8
8
  var LEGACY_SEARCH_TITLE = "This conversation began in Clay Studio's global search.";
@@ -25,6 +25,7 @@ function firstRecordedUserMessage(session) {
25
25
 
26
26
  function selectInitialTitle(session, transportText) {
27
27
  var existing = cleanText(session && session.title);
28
+ if (existing && session && session.titleProvisional === true) return { title: existing, provisional: true };
28
29
  if (existing) return { title: existing, provisional: false };
29
30
 
30
31
  // Prefer the visible message recorded in history. Query transports may wrap
@@ -41,9 +42,13 @@ function generationTurn(isMate) {
41
42
  return isMate === true ? HOME_GENERATION_TURN : DEFAULT_GENERATION_TURN;
42
43
  }
43
44
 
45
+ function isLegacySearchTitle(value) {
46
+ return cleanText(value).indexOf(LEGACY_SEARCH_TITLE) === 0;
47
+ }
48
+
44
49
  function repairLegacySearchTitle(session) {
45
50
  var title = cleanText(session && session.title);
46
- if (!session || title.indexOf(LEGACY_SEARCH_TITLE) !== 0) return false;
51
+ if (!session || !isLegacySearchTitle(title)) return false;
47
52
  var source = firstRecordedUserMessage(session);
48
53
  if (!source) return false;
49
54
  session.title = source.length > MAX_PROVISIONAL_CHARS ? source.substring(0, MAX_PROVISIONAL_CHARS) : source;
@@ -56,5 +61,6 @@ module.exports = {
56
61
  HOME_GENERATION_TURN: HOME_GENERATION_TURN,
57
62
  selectInitialTitle: selectInitialTitle,
58
63
  generationTurn: generationTurn,
64
+ isLegacySearchTitle: isLegacySearchTitle,
59
65
  repairLegacySearchTitle: repairLegacySearchTitle,
60
66
  };
package/lib/sessions.js CHANGED
@@ -8,6 +8,7 @@ var hygiene = require("./session-hygiene");
8
8
  var backgroundTaskTiming = require("./background-task-timing");
9
9
  var sessionProvenance = require("./session-provenance");
10
10
  var sessionVisibility = require("./session-visibility");
11
+ var sessionTitlePolicy = require("./session-title-policy");
11
12
  var projectAutonomousRun = require("./project-autonomous-run").projectAutonomousRun;
12
13
  var restoreStoredRun = require("./project-autonomous-run").restoreStoredRun;
13
14
 
@@ -153,6 +154,9 @@ function createSessionManager(opts) {
153
154
  localId: session.localId,
154
155
  cliSessionId: session.cliSessionId,
155
156
  title: session.title,
157
+ titleProvisional: session.titleProvisional === true,
158
+ titleAutoGenerated: session.titleAutoGenerated === true,
159
+ titleManuallySet: session.titleManuallySet === true,
156
160
  createdAt: session.createdAt,
157
161
  };
158
162
  if (session.ownerId) metaObj.ownerId = session.ownerId;
@@ -286,6 +290,11 @@ function createSessionManager(opts) {
286
290
  messageUUIDs.push({ uuid: loaded[i].history[k].uuid, type: loaded[i].history[k].messageType, historyIndex: k });
287
291
  }
288
292
  }
293
+ var hasTitleProvenance = Object.prototype.hasOwnProperty.call(m, "titleProvisional")
294
+ || Object.prototype.hasOwnProperty.call(m, "titleAutoGenerated")
295
+ || Object.prototype.hasOwnProperty.call(m, "titleManuallySet");
296
+ var legacyTitleProtected = !hasTitleProvenance && typeof m.title === "string"
297
+ && m.title.trim() && !sessionTitlePolicy.isLegacySearchTitle(m.title);
289
298
  var session = {
290
299
  localId: localId,
291
300
  queryInstance: null,
@@ -297,6 +306,9 @@ function createSessionManager(opts) {
297
306
  pendingAskUser: {},
298
307
  isProcessing: false,
299
308
  title: m.title || "",
309
+ titleProvisional: m.titleProvisional === true,
310
+ titleAutoGenerated: m.titleAutoGenerated === true,
311
+ titleManuallySet: m.titleManuallySet === true || legacyTitleProtected,
300
312
  createdAt: m.createdAt || Date.now(),
301
313
  lastActivity: loaded[i].mtime || m.createdAt || Date.now(),
302
314
  history: loaded[i].history,
@@ -643,6 +655,7 @@ function createSessionManager(opts) {
643
655
  allowedTools: {},
644
656
  isProcessing: false,
645
657
  title: "",
658
+ titleProvisional: false,
646
659
  titleAutoGenerated: false,
647
660
  turnCount: 0,
648
661
  createdAt: Date.now(),
@@ -691,6 +704,7 @@ function createSessionManager(opts) {
691
704
  allowedTools: {},
692
705
  isProcessing: false,
693
706
  title: "",
707
+ titleProvisional: false,
694
708
  titleAutoGenerated: false,
695
709
  turnCount: 0,
696
710
  createdAt: Date.now(),
@@ -12,6 +12,11 @@ function sessionIdForPersistence(session) {
12
12
  return session.cliSessionId || session.localId || null;
13
13
  }
14
14
 
15
+ function shouldPersistDisconnectPresence(existing, sessionId, existingSession, currentSession) {
16
+ return !!sessionId && (!existing || existing.sessionId === sessionId ||
17
+ (!!existingSession && !!currentSession && existingSession === currentSession));
18
+ }
19
+
15
20
  function findSession(sessions, sessionId) {
16
21
  if (!sessions || sessionId == null) return null;
17
22
  var found = null;
@@ -100,6 +105,7 @@ load();
100
105
 
101
106
  module.exports = {
102
107
  sessionIdForPersistence: sessionIdForPersistence,
108
+ shouldPersistDisconnectPresence: shouldPersistDisconnectPresence,
103
109
  findSession: findSession,
104
110
  setPresence: setPresence,
105
111
  getPresence: getPresence,
@@ -0,0 +1,65 @@
1
+ // Durable user-scoped context display preference storage.
2
+
3
+ var PREFERENCE_KEY = "contextViewPreference";
4
+ var MODES = ["off", "mini", "panel"];
5
+
6
+ function validMode(value) {
7
+ return MODES.indexOf(value) !== -1;
8
+ }
9
+
10
+ function attachContextViewPreferences(deps) {
11
+ var loadUsers = deps.loadUsers;
12
+ var saveUsers = deps.saveUsers;
13
+ var isMultiUser = deps.isMultiUser || function () { return false; };
14
+ var configModule = deps.configModule || require("./config");
15
+
16
+ function findRecord(data, userId) {
17
+ var records = Array.isArray(data.users) ? data.users : [];
18
+ for (var i = 0; i < records.length; i++) if (records[i] && records[i].id === userId) return records[i];
19
+ return null;
20
+ }
21
+
22
+ function result(value, source) {
23
+ if (value === undefined) return { present: false, mode: "off", source: null };
24
+ if (!validMode(value)) return { present: true, mode: "off", source: source, error: "Context display preference is invalid." };
25
+ return { present: true, mode: value, source: source };
26
+ }
27
+
28
+ function get(userId) {
29
+ var data;
30
+ try { data = loadUsers(); } catch (error) { return { present: false, mode: "off", source: null, error: "Unable to read context display preference: " + (error.message || error) }; }
31
+ var record = findRecord(data, userId);
32
+ if (isMultiUser() && !record) return { present: false, mode: "off", source: null, error: "User not found." };
33
+ if (record && Object.prototype.hasOwnProperty.call(record, PREFERENCE_KEY)) return result(record[PREFERENCE_KEY], "explicit");
34
+ if (record) return { present: false, mode: "off", source: null };
35
+ if (userId === "default") {
36
+ var config;
37
+ try { config = configModule.loadConfig() || {}; } catch (configError) { return { present: false, mode: "off", source: null, error: "Unable to read context display preference: " + (configError.message || configError) }; }
38
+ if (Object.prototype.hasOwnProperty.call(config, PREFERENCE_KEY)) return result(config[PREFERENCE_KEY], "explicit");
39
+ }
40
+ return { present: false, mode: "off", source: null };
41
+ }
42
+
43
+ function set(userId, mode) {
44
+ if (!validMode(mode)) return { ok: false, error: "Context display preference is invalid." };
45
+ var data;
46
+ try { data = loadUsers(); } catch (error) { return { ok: false, error: "Unable to read context display preference: " + (error.message || error) }; }
47
+ var record = findRecord(data, userId);
48
+ if (isMultiUser() && !record) return { ok: false, error: "User not found." };
49
+ if (record) {
50
+ var nextData = Object.assign({}, data, { users: (data.users || []).map(function (item) { return item === record ? Object.assign({}, item, { contextViewPreference: mode }) : item; }) });
51
+ try { saveUsers(nextData); } catch (saveError) { return { ok: false, error: "Unable to save context display preference: " + (saveError.message || saveError) }; }
52
+ return { ok: true, mode: mode };
53
+ }
54
+ if (userId !== "default") return { ok: false, error: "User not found." };
55
+ try {
56
+ var current = configModule.loadConfig() || {};
57
+ configModule.saveConfig(Object.assign({}, current, { contextViewPreference: mode }));
58
+ return { ok: true, mode: mode };
59
+ } catch (configSaveError) { return { ok: false, error: "Unable to save context display preference: " + (configSaveError.message || configSaveError) }; }
60
+ }
61
+
62
+ return { get: get, set: set, validMode: validMode, MODES: MODES };
63
+ }
64
+
65
+ module.exports = { attachContextViewPreferences: attachContextViewPreferences, validMode: validMode, MODES: MODES, PREFERENCE_KEY: PREFERENCE_KEY };
package/lib/users.js CHANGED
@@ -12,6 +12,7 @@ var { attachExperimentalPreferences } = require("./users-experimental-preference
12
12
  var { attachScheduledTaskPreferences } = require("./users-scheduled-task-preferences");
13
13
  var { attachDefaultAiPreferences } = require("./users-default-ai-preferences");
14
14
  var { attachDefaultVendorPreferences } = require("./users-default-vendor-preferences");
15
+ var { attachContextViewPreferences } = require("./users-context-view-preferences");
15
16
 
16
17
  var USERS_FILE = path.join(CONFIG_DIR, "users.json");
17
18
 
@@ -406,6 +407,7 @@ var experimentalPreferences = attachExperimentalPreferences({ loadUsers: loadUse
406
407
  var scheduledTaskPreferences = attachScheduledTaskPreferences({ loadUsers: loadUsers, saveUsers: saveUsers });
407
408
  var defaultAiPreferences = attachDefaultAiPreferences({ loadUsers: loadUsers, saveUsers: saveUsers, isMultiUser: auth.isMultiUser });
408
409
  var defaultVendorPreferences = attachDefaultVendorPreferences({ loadUsers: loadUsers, saveUsers: saveUsers, isMultiUser: auth.isMultiUser });
410
+ var contextViewPreferences = attachContextViewPreferences({ loadUsers: loadUsers, saveUsers: saveUsers, isMultiUser: auth.isMultiUser });
409
411
 
410
412
  // Alias auth functions
411
413
  var isMultiUser = auth.isMultiUser;
@@ -468,6 +470,8 @@ var getDefaultAiPreference = defaultAiPreferences.getDefaultAiPreference;
468
470
  var setDefaultAiPreference = defaultAiPreferences.setDefaultAiPreference;
469
471
  var getDefaultVendorPreference = defaultVendorPreferences.getDefaultVendor;
470
472
  var setDefaultVendorPreference = defaultVendorPreferences.setDefaultVendor;
473
+ var getContextViewPreference = contextViewPreferences.get;
474
+ var setContextViewPreference = contextViewPreferences.set;
471
475
 
472
476
  module.exports = {
473
477
  USERS_FILE: USERS_FILE,
@@ -551,6 +555,8 @@ module.exports = {
551
555
  setDefaultAiPreference: setDefaultAiPreference,
552
556
  getDefaultVendorPreference: getDefaultVendorPreference,
553
557
  setDefaultVendorPreference: setDefaultVendorPreference,
558
+ getContextViewPreference: getContextViewPreference,
559
+ setContextViewPreference: setContextViewPreference,
554
560
  getDeletedBuiltinKeys: getDeletedBuiltinKeys,
555
561
  addDeletedBuiltinKey: addDeletedBuiltinKey,
556
562
  removeDeletedBuiltinKey: removeDeletedBuiltinKey,
package/lib/ws-schema.js CHANGED
@@ -726,6 +726,9 @@ var schema = {
726
726
  "default_vendor_get": { direction: "c2s", handler: "lib/server-default-vendor.js", description: "Read the authenticated user's New session vendor preference" },
727
727
  "default_vendor_set": { direction: "c2s", handler: "lib/server-default-vendor.js", description: "Validate and persist the authenticated user's New session vendor preference" },
728
728
  "default_vendor_state": { direction: "s2c", handler: "lib/public/modules/default-vendor.js", description: "Correlated or same-user-broadcast New session vendor preference" },
729
+ "context_view_get": { direction: "c2s", handler: "lib/server-context-view.js", description: "Read the authenticated user's context display preference" },
730
+ "context_view_set": { direction: "c2s", handler: "lib/server-context-view.js", description: "Persist the authenticated user's context display preference" },
731
+ "context_view_state": { direction: "s2c", handler: "lib/public/modules/context-view-preference.js", description: "Correlated or same-user-broadcast context display preference" },
729
732
  "default_ai_state": { direction: "s2c", handler: "lib/public/modules/default-ai.js", description: "Correlated or same-user-broadcast canonical Default AI state" },
730
733
  "default_ai_catalog": { direction: "s2c", handler: "lib/public/modules/default-ai.js", description: "Correlated Default AI provider catalog" },
731
734
  "home_mate_history": { direction: "s2c", handler: "lib/public/modules/home-mate-chat.js", description: "Selected mate's initial or refreshed chat history" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "4.1.0-beta.15",
3
+ "version": "4.1.0-beta.16",
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"