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.
- package/lib/project-connection.js +5 -1
- package/lib/project-sessions.js +2 -0
- package/lib/project-user-message.js +4 -0
- package/lib/project.js +1 -1
- package/lib/public/app.js +4 -0
- package/lib/public/modules/app-connection.js +6 -1
- package/lib/public/modules/app-messages.js +12 -4
- package/lib/public/modules/app-panels.js +21 -10
- package/lib/public/modules/app-projects.js +2 -0
- package/lib/public/modules/context-view-preference.js +143 -0
- package/lib/public/modules/pane-bridge.js +8 -8
- package/lib/public/modules/project-activation.js +49 -0
- package/lib/public/modules/split-session-boundary.js +28 -0
- package/lib/public/modules/split-view.js +40 -7
- package/lib/sdk-bridge.js +11 -44
- package/lib/sdk-message-processor.js +4 -2
- package/lib/server-context-view.js +53 -0
- package/lib/server.js +4 -0
- package/lib/session-title-generator.js +89 -0
- package/lib/session-title-policy.js +8 -2
- package/lib/sessions.js +14 -0
- package/lib/user-presence.js +6 -0
- package/lib/users-context-view-preferences.js +65 -0
- package/lib/users.js +6 -0
- package/lib/ws-schema.js +3 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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);
|
package/lib/project-sessions.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
903
|
+
return getStoredContextView();
|
|
893
904
|
}
|
|
894
905
|
|
|
895
906
|
export function setContextView(v) {
|
|
896
|
-
|
|
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(
|
|
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 =
|
|
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 {
|
|
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
|
|
42
|
-
//
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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) {
|
|
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
|
|
665
|
-
|
|
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 =
|
|
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
|
|
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(),
|
package/lib/user-presence.js
CHANGED
|
@@ -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