clay-server 4.1.0-beta.9 → 4.2.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/lib/context-usage.js +47 -0
  2. package/lib/daemon-project-access.js +14 -0
  3. package/lib/daemon.js +7 -0
  4. package/lib/issues-mcp-server.js +1 -1
  5. package/lib/issues-service.js +3 -1
  6. package/lib/loop-registry-occurrence.js +1 -0
  7. package/lib/notes.js +20 -3
  8. package/lib/os-users.js +6 -0
  9. package/lib/project-connection.js +14 -3
  10. package/lib/project-file-path.js +13 -0
  11. package/lib/project-file-watch.js +19 -8
  12. package/lib/project-filesystem.js +10 -16
  13. package/lib/project-http.js +53 -48
  14. package/lib/project-issue-launch.js +3 -1
  15. package/lib/project-logs-mcp-server.js +8 -8
  16. package/lib/project-logs-schema.js +1 -1
  17. package/lib/project-logs-service.js +46 -13
  18. package/lib/project-logs-store.js +17 -15
  19. package/lib/project-logs.js +70 -0
  20. package/lib/project-loop.js +1 -0
  21. package/lib/project-notifications.js +1 -1
  22. package/lib/project-scheduled-result-store.js +131 -0
  23. package/lib/project-scheduled-task-execution.js +35 -2
  24. package/lib/project-session-notes.js +11 -4
  25. package/lib/project-sessions.js +39 -6
  26. package/lib/project-user-message.js +47 -0
  27. package/lib/project.js +57 -3
  28. package/lib/public/app.js +27 -9
  29. package/lib/public/css/avatar-imprints.css +1 -1
  30. package/lib/public/css/command-palette.css +57 -19
  31. package/lib/public/css/git-panel.css +13 -12
  32. package/lib/public/css/git-placard.css +4 -4
  33. package/lib/public/css/issues.css +79 -32
  34. package/lib/public/css/mobile-nav.css +7 -0
  35. package/lib/public/css/notifications-center.css +3 -3
  36. package/lib/public/css/project-logs.css +15 -0
  37. package/lib/public/css/sidebar.css +7 -0
  38. package/lib/public/css/sticky-notes.css +29 -0
  39. package/lib/public/index.html +10 -0
  40. package/lib/public/modules/app-connection.js +9 -1
  41. package/lib/public/modules/app-favicon.js +10 -0
  42. package/lib/public/modules/app-messages.js +50 -12
  43. package/lib/public/modules/app-notifications.js +2 -0
  44. package/lib/public/modules/app-panels.js +77 -23
  45. package/lib/public/modules/app-projects.js +2 -0
  46. package/lib/public/modules/app-rate-limit.js +148 -61
  47. package/lib/public/modules/avatar-imprint.js +25 -70
  48. package/lib/public/modules/avatar.js +12 -4
  49. package/lib/public/modules/chat-render-runtime.js +20 -0
  50. package/lib/public/modules/clay-issue-links.js +24 -2
  51. package/lib/public/modules/clay-log-links.js +12 -1
  52. package/lib/public/modules/context-view-preference.js +143 -0
  53. package/lib/public/modules/default-vendor.js +116 -0
  54. package/lib/public/modules/filebrowser-tabs.js +1 -0
  55. package/lib/public/modules/filebrowser.js +45 -18
  56. package/lib/public/modules/issues-render.js +36 -10
  57. package/lib/public/modules/issues.js +95 -13
  58. package/lib/public/modules/markdown.js +3 -1
  59. package/lib/public/modules/pane-bridge.js +14 -8
  60. package/lib/public/modules/pane-issue-bridge.js +27 -0
  61. package/lib/public/modules/permission-hints.js +13 -0
  62. package/lib/public/modules/profile.js +9 -9
  63. package/lib/public/modules/project-activation.js +49 -0
  64. package/lib/public/modules/project-logs-render.js +13 -3
  65. package/lib/public/modules/project-logs.js +45 -15
  66. package/lib/public/modules/project-session-visibility-settings.js +138 -0
  67. package/lib/public/modules/project-settings.js +5 -0
  68. package/lib/public/modules/right-workbench.js +44 -0
  69. package/lib/public/modules/scheduled-result-client.js +33 -0
  70. package/lib/public/modules/scheduled-result-display.js +50 -0
  71. package/lib/public/modules/scheduled-result-notification.js +31 -0
  72. package/lib/public/modules/scheduled-task-presentation.js +15 -0
  73. package/lib/public/modules/scheduled-tasks.js +9 -21
  74. package/lib/public/modules/sidebar-mobile.js +51 -3
  75. package/lib/public/modules/sidebar-projects.js +14 -3
  76. package/lib/public/modules/sidebar-sessions.js +65 -5
  77. package/lib/public/modules/split-session-boundary.js +28 -0
  78. package/lib/public/modules/split-view.js +42 -7
  79. package/lib/public/modules/sticky-notes-browser.js +130 -18
  80. package/lib/public/modules/sticky-notes.js +2 -3
  81. package/lib/public/modules/terminal.js +4 -4
  82. package/lib/public/modules/thinking-lifecycle.js +4 -1
  83. package/lib/public/modules/tools.js +5 -1
  84. package/lib/scheduled-run-state.js +10 -2
  85. package/lib/scheduler.js +1 -0
  86. package/lib/sdk-bridge.js +97 -61
  87. package/lib/sdk-message-processor.js +44 -13
  88. package/lib/sdk-skill-discovery.js +6 -28
  89. package/lib/sdk-turn-queue.js +14 -0
  90. package/lib/server-context-view.js +53 -0
  91. package/lib/server-default-vendor.js +60 -0
  92. package/lib/server.js +31 -7
  93. package/lib/session-notes-mcp-server.js +3 -5
  94. package/lib/session-pair-factory.js +2 -2
  95. package/lib/session-title-generator.js +89 -0
  96. package/lib/session-title-policy.js +8 -2
  97. package/lib/session-visibility.js +62 -1
  98. package/lib/sessions.js +28 -1
  99. package/lib/user-presence.js +6 -0
  100. package/lib/users-context-view-preferences.js +65 -0
  101. package/lib/users-default-vendor-preferences.js +80 -0
  102. package/lib/users.js +12 -0
  103. package/lib/ws-schema.js +13 -0
  104. package/lib/yoke/acp-agent-profiles.js +15 -2
  105. package/lib/yoke/adapters/acp.js +3 -2
  106. package/lib/yoke/adapters/antigravity.js +2 -2
  107. package/lib/yoke/adapters/claude-worker.js +11 -12
  108. package/lib/yoke/adapters/claude.js +75 -22
  109. package/lib/yoke/adapters/codex.js +216 -29
  110. package/lib/yoke/adapters/kiro.js +6 -5
  111. package/lib/yoke/claude-context-usage.js +57 -0
  112. package/lib/yoke/claude-message-correlation.js +31 -0
  113. package/lib/yoke/claude-model-switch.js +15 -0
  114. package/lib/yoke/index.js +5 -1
  115. package/lib/yoke/mcp-bridge-server.js +18 -5
  116. package/lib/yoke/session-skill-catalog.js +45 -0
  117. package/lib/yoke/skill-discovery.js +60 -20
  118. package/lib/yoke/vendor-registry.js +1 -1
  119. package/package.json +5 -7
package/lib/server.js CHANGED
@@ -17,6 +17,8 @@ var serverHomeChat = require("./server-home-chat");
17
17
  var shouldSuppressHomeResponse = require("./home-response-notifications").shouldSuppressHomeResponse;
18
18
  var serverHomePreferences = require("./server-home-preferences");
19
19
  var serverDefaultAi = require("./server-default-ai");
20
+ var serverDefaultVendor = require("./server-default-vendor");
21
+ var serverContextView = require("./server-context-view");
20
22
  var serverCursorSharing = require("./server-cursor-sharing");
21
23
  var serverTools = require("./server-tools");
22
24
  var serverAdmin = require("./server-admin");
@@ -156,6 +158,7 @@ function createServer(opts) {
156
158
  var onReorderProjects = opts.onReorderProjects || null;
157
159
  var onSetProjectTitle = opts.onSetProjectTitle || null;
158
160
  var onSetProjectIcon = opts.onSetProjectIcon || null;
161
+ var onSetProjectSessionVisibilityDefault = opts.onSetProjectSessionVisibilityDefault || null;
159
162
  var onProjectOwnerChanged = opts.onProjectOwnerChanged || null;
160
163
  var onGetServerDefaultEffort = opts.onGetServerDefaultEffort || null;
161
164
  var onSetServerDefaultEffort = opts.onSetServerDefaultEffort || null;
@@ -943,9 +946,9 @@ function createServer(opts) {
943
946
  return;
944
947
  }
945
948
 
946
- // Auth check for project routes
947
- // Bypass auth for MCP bridge endpoint (localhost only).
948
- // The mcp-bridge-server.js runs as a local child process and cannot carry cookies.
949
+ // Auth check for project routes. The local MCP bridge gets a narrowly
950
+ // scoped project capability from its parent process; localhost alone is
951
+ // not sufficient because browser/local callers must not reach the bridge.
949
952
  var projectUrlForAuth = stripPrefix(req.url.split("?")[0], slug);
950
953
  // req.socket.remoteAddress may differ between HTTP/HTTPS (TLSSocket wraps net.Socket).
951
954
  // Also check req.connection.remoteAddress for compatibility.
@@ -956,10 +959,18 @@ function createServer(opts) {
956
959
  var isMcpBridgeLocal = projectUrlForAuth === "/api/mcp-bridge"
957
960
  && req.method === "POST"
958
961
  && isLocalhost;
962
+ var isMcpBridgeAuthenticated = isMcpBridgeLocal
963
+ && typeof ctx.isMcpBridgeAuthenticated === "function"
964
+ && ctx.isMcpBridgeAuthenticated(req);
959
965
  if (projectUrlForAuth === "/api/mcp-bridge") {
960
- console.log("[server] MCP bridge auth: method=" + req.method + " addr=" + remoteAddr + " bypass=" + isMcpBridgeLocal);
966
+ console.log("[server] MCP bridge auth: method=" + req.method + " addr=" + remoteAddr + " capability=" + isMcpBridgeAuthenticated);
961
967
  }
962
- if (!isMcpBridgeLocal && !isRequestAuthed(req)) {
968
+ if (projectUrlForAuth === "/api/mcp-bridge" && !isMcpBridgeAuthenticated) {
969
+ res.writeHead(403, { "Content-Type": "application/json" });
970
+ res.end('{"error":"MCP bridge capability required"}');
971
+ return;
972
+ }
973
+ if (!isMcpBridgeAuthenticated && !isRequestAuthed(req)) {
963
974
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
964
975
  res.end(auth.getAuthPage());
965
976
  return;
@@ -969,7 +980,7 @@ function createServer(opts) {
969
980
  res.setHeader("Set-Cookie", "clay_last_project=" + slug + "; Path=/; SameSite=Strict; Max-Age=31536000" + (tlsOptions ? "; Secure" : ""));
970
981
 
971
982
  // Multi-user: check project access for HTTP requests
972
- if (users.isMultiUser()) {
983
+ if (users.isMultiUser() && !isMcpBridgeAuthenticated) {
973
984
  var httpUser = getMultiUserFromReq(req);
974
985
  if (!httpUser || !canUserAccessSlug(httpUser.id, slug)) {
975
986
  res.writeHead(302, { "Location": "/" });
@@ -1067,6 +1078,8 @@ function createServer(opts) {
1067
1078
  var wss = new WebSocketServer({ noServer: true });
1068
1079
  var homePreferencesHandler = null;
1069
1080
  var defaultAiService = null;
1081
+ var defaultVendorService = null;
1082
+ var contextViewService = null;
1070
1083
  var cursorSharingService = null;
1071
1084
 
1072
1085
  // Slug-less /ws handler: lets a user with no projects yet load the regular
@@ -1321,6 +1334,7 @@ function createServer(opts) {
1321
1334
  projectOwnerId: projectOwnerId || null,
1322
1335
  projectKnowledgeId: extra.projectKnowledgeId || (worktreeMeta && worktreeMeta.projectKnowledgeId) || null,
1323
1336
  worktreeMeta: worktreeMeta || null,
1337
+ mcpBridgeToken: extra.mcpBridgeToken || null,
1324
1338
  isMate: extra.isMate || false,
1325
1339
  mateId: extra.mateId || (extra.isMate ? path.basename(cwd) : null),
1326
1340
  mateDisplayName: extra.mateDisplayName || "",
@@ -1445,6 +1459,7 @@ function createServer(opts) {
1445
1459
  onReorderProjects: onReorderProjects,
1446
1460
  onSetProjectTitle: onSetProjectTitle,
1447
1461
  onSetProjectIcon: onSetProjectIcon,
1462
+ onSetProjectSessionVisibilityDefault: onSetProjectSessionVisibilityDefault,
1448
1463
  onProjectOwnerChanged: onProjectOwnerChanged,
1449
1464
  onGetServerDefaultEffort: onGetServerDefaultEffort,
1450
1465
  onSetServerDefaultEffort: onSetServerDefaultEffort,
@@ -1552,6 +1567,8 @@ function createServer(opts) {
1552
1567
  });
1553
1568
 
1554
1569
  defaultAiService = serverDefaultAi.attachDefaultAiService({ users: users, homeChatHandler: homeChatHandler, forEachAppClient: forEachAppClient });
1570
+ defaultVendorService = serverDefaultVendor.attachDefaultVendorService({ users: users, homeChatHandler: homeChatHandler, forEachAppClient: forEachAppClient });
1571
+ contextViewService = serverContextView.attachContextViewService({ users: users, forEachAppClient: forEachAppClient });
1555
1572
  cursorSharingService = serverCursorSharing.attachCursorSharingService({ users: users, forEachAppClient: forEachAppClient });
1556
1573
 
1557
1574
  // --- User-installed home tools handler ---
@@ -1580,6 +1597,8 @@ function createServer(opts) {
1580
1597
 
1581
1598
  function handleDmMessage(ws, msg, routedProjectSlug) {
1582
1599
  if (cursorSharingService && cursorSharingService.handleMessage(ws, msg)) return true;
1600
+ if (defaultVendorService && defaultVendorService.handleMessage(ws, msg, routedProjectSlug)) return true;
1601
+ if (contextViewService && contextViewService.handleMessage(ws, msg)) return true;
1583
1602
  if (defaultAiService && defaultAiService.handleMessage(ws, msg)) return true;
1584
1603
  if (assignmentService.handleMessage(ws, msg, routedProjectSlug)) return true;
1585
1604
  if (dmHandler.handleMessage(ws, msg)) return true;
@@ -1690,7 +1709,12 @@ function createServer(opts) {
1690
1709
  var visible = [];
1691
1710
  projects.forEach(function (ctx, slug) {
1692
1711
  if (users.isMultiUser() && (!userId || !canUserAccessSlug(userId, slug))) return;
1693
- visible.push(ctx.getStatus());
1712
+ var status = ctx.getStatus();
1713
+ if (!status.isMate && onGetProjectAccess) {
1714
+ var access = onGetProjectAccess(slug);
1715
+ status.sessionVisibilityDefault = access && access.sessionVisibilityDefault === "shared" ? "shared" : "private";
1716
+ }
1717
+ visible.push(status);
1694
1718
  });
1695
1719
  return visible;
1696
1720
  }
@@ -4,10 +4,8 @@ var buildShape = require("./session-spawn-mcp-server").buildShape;
4
4
 
5
5
  var MEMORY_CONTRACT =
6
6
  "The sticky-note board persists across sessions and is a user-facing artifact shared with people and Clay agents, not private agent scratch space. " +
7
- "Default to not writing. Create a note proactively only when the user explicitly asks to remember or track something, or when all of these are true: it will remain useful after the current task and session, it is not already adequately recorded in the repository or another note, and the user would likely be glad to find it on the board a week later. " +
8
- "Good notes capture an unresolved commitment, durable product decision, user preference, constraint, or handoff that will materially change future work. " +
9
- "Important exception for deferred defects: while doing code or technical work, actively create a sticky note when you discover a concrete defect, regression risk, security issue, or data-loss risk that is outside the current session goal and will remain unfixed when the turn ends. Do not wait for the user to ask. For a Project Driver session with Issues authority, search, reuse, or create the Issue first and put the primary details there: observable evidence, affected component, impact, and next action. Then make the sticky note only a concise title, one-line actionable cue, and the actual opaque issue: reference; do not duplicate the full defect details in the note. If you lack Issues authority, route through an eligible Project Driver; never invent an issue reference or drop the alert. Check active notes first when a duplicate is plausible. Do not create defect notes for speculation, general cleanup ideas, or problems you fixed in the current session. " +
10
- "Important exception for deferred proposals: when you propose work (a fix, follow-up, improvement, or next step) and the user defers it rather than declining it (\"let's do that later\", \"next time\", \"after this\"), actively create a sticky note before the topic moves on. Do not wait to be asked. Capture what was proposed, why it mattered, and the agreed timing if any. Deferred agreements scroll out of chat history quickly and are hard to track; the board is where they survive. If the user declines the idea outright, do not write a note. " +
7
+ "Default to not writing. Create a note proactively only when the user explicitly asks to remember or track something, or when all of these are true: it will remain useful after the current task and session, it is not already adequately recorded in the repository, an Issue, or another note, and the user would likely be glad to find it on the board a week later. " +
8
+ "Use Sticky Notes for preferences, reminders, undeveloped ideas, or an explicit request to remember something; they are not mandatory duplicates of Project Issues. For concrete actionable bugs, improvements, or deferred implementation, an authorized Project Driver should use Project Issues. If you lack Issues authority, route the work through an eligible Project Driver. Check active notes first when a duplicate is plausible. " +
11
9
  "Never create a note merely because work is important, lengthy, spans agents or restarts, or might help another agent. Do not record completed work, implementation details, test results, investigation logs, transient blockers, conversation summaries, or announcements of your own activity. When uncertain, do not write. " +
12
10
  "Updates are visible too: update only when durable state materially changes. " +
13
11
  "A note is an attention item, so finishing it means closing it, never erasing it: call close_note once the thing it is asking for is done, and the note leaves the active board while the record stays and can be reopened. Do not turn a note into a completion log instead of closing it. " +
@@ -35,7 +33,7 @@ function getToolDefs(handlers) {
35
33
  },
36
34
  {
37
35
  name: "close_note",
38
- description: MEMORY_CONTRACT + " Close a note created by this same session once what it asked for is done. Closing is not deletion: the note leaves the active board, stays queryable under state closed, and can be reopened. Notes created by users or other sessions cannot be closed by this session.",
36
+ description: MEMORY_CONTRACT + " Close a note once what it asked for is done. Authorized Project Drivers may close any note in their project, including notes created by people or other sessions. Other sessions may only close their own notes. Closing is not deletion: the note leaves the active board, stays queryable under state closed, and can be reopened.",
39
37
  inputSchema: buildShape({
40
38
  id: { type: "string", description: "Id of the note to close." },
41
39
  }, ["id"]),
@@ -235,12 +235,12 @@ function attachPairFactory(ctx) {
235
235
  var request = args || {};
236
236
  var driverRuntime = preflightRuntime(request.driver || {}, "claude");
237
237
  var workerRuntime = preflightRuntime(request.worker || {}, null);
238
- var driver = sm.createSessionRaw({ ownerId: ownerId || null, vendor: driverRuntime.vendor, model: driverRuntime.model, effort: driverRuntime.effort });
238
+ var driver = sm.createSessionRaw({ ownerId: ownerId || null, vendor: driverRuntime.vendor, model: driverRuntime.model, effort: driverRuntime.effort, hidden: request.hidden === true });
239
239
  var worker = null;
240
240
  try {
241
241
  driver.title = request.name ? request.name + " · Driver" : "Scheduled task · Driver";
242
242
  if (driverRuntime.explicitEffort) driver.loopSettings = { effort: driverRuntime.effort };
243
- worker = sm.createSessionRaw({ ownerId: ownerId || null, vendor: workerRuntime.vendor, model: workerRuntime.model, effort: workerRuntime.effort });
243
+ worker = sm.createSessionRaw({ ownerId: ownerId || null, vendor: workerRuntime.vendor, model: workerRuntime.model, effort: workerRuntime.effort, hidden: request.hidden === true });
244
244
  worker.title = request.name ? request.name + " · Split Worker" : "Scheduled task · Split Worker";
245
245
  if (workerRuntime.explicitEffort) worker.loopSettings = { effort: workerRuntime.effort };
246
246
  sessionProvenance.markWorker(driver, worker, sm.sessions);
@@ -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
  };
@@ -7,6 +7,66 @@ function restore(meta) {
7
7
  return meta.sessionVisibilityExplicit === true ? normalize(meta.sessionVisibility) : "private";
8
8
  }
9
9
 
10
+ function defaultForProject(access) {
11
+ return access && access.sessionVisibilityDefault === "shared" ? "shared" : "private";
12
+ }
13
+
14
+ function resolveNewSessionVisibility(access, msg, isMate) {
15
+ if (isMate === true) return { visibility: "private" };
16
+ if (!msg || !Object.prototype.hasOwnProperty.call(msg, "sessionVisibility")) {
17
+ return { visibility: defaultForProject(access) };
18
+ }
19
+ if (msg.sessionVisibility === "private" || msg.sessionVisibility === "shared") {
20
+ return { visibility: msg.sessionVisibility };
21
+ }
22
+ return { error: "Session visibility must be private or shared." };
23
+ }
24
+
25
+ function defaultChangeResult(ctx, ws, msg, result) {
26
+ var requestedSlug = typeof msg.slug === "string" && msg.slug.length > 0 && msg.slug.length <= 200 ? msg.slug : ctx.slug;
27
+ ctx.sendTo(ws, Object.assign({
28
+ type: "set_project_session_visibility_default_result",
29
+ slug: requestedSlug,
30
+ requestId: typeof msg.requestId === "string" ? msg.requestId.substring(0, 200) : null,
31
+ }, result));
32
+ }
33
+
34
+ function handleDefaultChange(ctx, ws, msg) {
35
+ if (!msg || (msg.visibility !== "shared" && msg.visibility !== "private")) {
36
+ defaultChangeResult(ctx, ws, msg || {}, { ok: false, error: "Session visibility default must be private or shared." });
37
+ return true;
38
+ }
39
+ if (msg.slug && msg.slug !== ctx.slug) {
40
+ defaultChangeResult(ctx, ws, msg, { ok: false, error: "Project settings access is not permitted" });
41
+ return true;
42
+ }
43
+ if (ctx.isMate === true || !ctx.usersModule || !ctx.usersModule.isMultiUser || !ctx.usersModule.isMultiUser()) {
44
+ defaultChangeResult(ctx, ws, msg, { ok: false, error: "Not supported" });
45
+ return true;
46
+ }
47
+ var user = ws && ws._clayUser && ctx.usersModule.findUserById(ws._clayUser.id);
48
+ var access = ctx.getProjectAccess ? ctx.getProjectAccess() : null;
49
+ if (access && access.isWorktree === true) {
50
+ defaultChangeResult(ctx, ws, msg, { ok: false, error: "Worktrees inherit this setting from their parent project." });
51
+ return true;
52
+ }
53
+ var permissions = user && ctx.usersModule.getEffectivePermissions
54
+ ? ctx.usersModule.getEffectivePermissions(user, ctx.osUsers) : {};
55
+ var isOwner = !!(user && access && access.ownerId && access.ownerId === user.id);
56
+ var isAdmin = !!(user && user.role === "admin");
57
+ if (!user || !permissions.projectSettings || (!isOwner && !isAdmin)) {
58
+ defaultChangeResult(ctx, ws, msg, { ok: false, error: "Project settings access is not permitted" });
59
+ return true;
60
+ }
61
+ if (typeof ctx.opts.onSetProjectSessionVisibilityDefault !== "function") {
62
+ defaultChangeResult(ctx, ws, msg, { ok: false, error: "Not supported" });
63
+ return true;
64
+ }
65
+ var result = ctx.opts.onSetProjectSessionVisibilityDefault(ctx.slug, msg.visibility);
66
+ defaultChangeResult(ctx, ws, msg, { ok: !!(result && result.ok), visibility: result && result.visibility, error: result && result.error });
67
+ return true;
68
+ }
69
+
10
70
  function handleChange(ctx, ws, msg) {
11
71
  if (typeof msg.sessionId !== "number" || (msg.visibility !== "shared" && msg.visibility !== "private")) return;
12
72
  var session = ctx.sm.sessions.get(msg.sessionId);
@@ -38,4 +98,5 @@ function revokeViewers(ctx, session) {
38
98
  }
39
99
  }
40
100
 
41
- module.exports = { normalize: normalize, restore: restore, handleChange: handleChange, revokeViewers: revokeViewers };
101
+ module.exports = { normalize: normalize, restore: restore, defaultForProject: defaultForProject, resolveNewSessionVisibility: resolveNewSessionVisibility,
102
+ handleChange: handleChange, handleDefaultChange: handleDefaultChange, revokeViewers: revokeViewers };
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
 
@@ -23,6 +24,7 @@ function createSessionManager(opts) {
23
24
  var sessionViewedListeners = [];
24
25
  var onSessionRenamed = opts.onSessionRenamed || function () {};
25
26
  var onSessionIdentityAssigned = opts.onSessionIdentityAssigned || function () {};
27
+ var onSessionHydrate = opts.onSessionHydrate || null;
26
28
 
27
29
  // --- Multi-session state ---
28
30
  var nextLocalId = 1;
@@ -153,11 +155,15 @@ function createSessionManager(opts) {
153
155
  localId: session.localId,
154
156
  cliSessionId: session.cliSessionId,
155
157
  title: session.title,
158
+ titleProvisional: session.titleProvisional === true,
159
+ titleAutoGenerated: session.titleAutoGenerated === true,
160
+ titleManuallySet: session.titleManuallySet === true,
156
161
  createdAt: session.createdAt,
157
162
  };
158
163
  if (session.ownerId) metaObj.ownerId = session.ownerId;
159
164
  if (session.vendor) metaObj.vendor = session.vendor;
160
165
  if (session.codexDynamicToolCatalogVersion) metaObj.codexDynamicToolCatalogVersion = session.codexDynamicToolCatalogVersion;
166
+ if (session.codexIssuesToolCatalogVersion) metaObj.codexIssuesToolCatalogVersion = session.codexIssuesToolCatalogVersion;
161
167
  if (session.model) metaObj.model = session.model;
162
168
  if (session.effort) metaObj.effort = session.effort;
163
169
  // Persist the session's "born" mode so TUI sessions reappear after a
@@ -177,6 +183,7 @@ function createSessionManager(opts) {
177
183
  if (session.loopInterviewHandoff) metaObj.loopInterviewHandoff = session.loopInterviewHandoff;
178
184
  if (session.scheduledTaskDraft) metaObj.scheduledTaskDraft = session.scheduledTaskDraft;
179
185
  if (session.scheduledTaskRun) metaObj.scheduledTaskRun = session.scheduledTaskRun;
186
+ if (session.hidden) metaObj.hidden = true;
180
187
  metaObj.sessionVisibility = sessionVisibility.normalize(session.sessionVisibility);
181
188
  if (session.sessionVisibilityExplicit) metaObj.sessionVisibilityExplicit = true;
182
189
  if (session.bookmarked) metaObj.bookmarked = true;
@@ -285,6 +292,11 @@ function createSessionManager(opts) {
285
292
  messageUUIDs.push({ uuid: loaded[i].history[k].uuid, type: loaded[i].history[k].messageType, historyIndex: k });
286
293
  }
287
294
  }
295
+ var hasTitleProvenance = Object.prototype.hasOwnProperty.call(m, "titleProvisional")
296
+ || Object.prototype.hasOwnProperty.call(m, "titleAutoGenerated")
297
+ || Object.prototype.hasOwnProperty.call(m, "titleManuallySet");
298
+ var legacyTitleProtected = !hasTitleProvenance && typeof m.title === "string"
299
+ && m.title.trim() && !sessionTitlePolicy.isLegacySearchTitle(m.title);
288
300
  var session = {
289
301
  localId: localId,
290
302
  queryInstance: null,
@@ -296,6 +308,9 @@ function createSessionManager(opts) {
296
308
  pendingAskUser: {},
297
309
  isProcessing: false,
298
310
  title: m.title || "",
311
+ titleProvisional: m.titleProvisional === true,
312
+ titleAutoGenerated: m.titleAutoGenerated === true,
313
+ titleManuallySet: m.titleManuallySet === true || legacyTitleProtected,
299
314
  createdAt: m.createdAt || Date.now(),
300
315
  lastActivity: loaded[i].mtime || m.createdAt || Date.now(),
301
316
  history: loaded[i].history,
@@ -304,6 +319,7 @@ function createSessionManager(opts) {
304
319
  };
305
320
  if (m.vendor) session.vendor = m.vendor;
306
321
  session.codexDynamicToolCatalogVersion = Number(m.codexDynamicToolCatalogVersion || 0);
322
+ session.codexIssuesToolCatalogVersion = Number(m.codexIssuesToolCatalogVersion || 0);
307
323
  session.model = m.model || deriveSessionModel(loaded[i].history);
308
324
  session.effort = m.effort || null;
309
325
  if (m.loop) session.loop = m.loop;
@@ -338,6 +354,7 @@ function createSessionManager(opts) {
338
354
  session.loopInterviewHandoff = m.loopInterviewHandoff && typeof m.loopInterviewHandoff === "object" ? m.loopInterviewHandoff : null;
339
355
  session.scheduledTaskDraft = m.scheduledTaskDraft && typeof m.scheduledTaskDraft === "object" ? m.scheduledTaskDraft : null;
340
356
  session.scheduledTaskRun = m.scheduledTaskRun && typeof m.scheduledTaskRun === "object" ? m.scheduledTaskRun : null;
357
+ session.hidden = m.hidden === true || !!session.scheduledTaskRun;
341
358
  session.terminalId = null;
342
359
  session.runtimeMode = null;
343
360
  session.runtimeTerminalId = null;
@@ -641,6 +658,7 @@ function createSessionManager(opts) {
641
658
  allowedTools: {},
642
659
  isProcessing: false,
643
660
  title: "",
661
+ titleProvisional: false,
644
662
  titleAutoGenerated: false,
645
663
  turnCount: 0,
646
664
  createdAt: Date.now(),
@@ -664,6 +682,7 @@ function createSessionManager(opts) {
664
682
  loopInterviewHandoff: null,
665
683
  scheduledTaskDraft: null,
666
684
  scheduledTaskRun: null,
685
+ hidden: !!(sessionOpts && sessionOpts.hidden),
667
686
  terminalId: null,
668
687
  sessionOriginId: null,
669
688
  };
@@ -688,6 +707,7 @@ function createSessionManager(opts) {
688
707
  allowedTools: {},
689
708
  isProcessing: false,
690
709
  title: "",
710
+ titleProvisional: false,
691
711
  titleAutoGenerated: false,
692
712
  turnCount: 0,
693
713
  createdAt: Date.now(),
@@ -711,6 +731,7 @@ function createSessionManager(opts) {
711
731
  loopInterviewHandoff: null,
712
732
  scheduledTaskDraft: null,
713
733
  scheduledTaskRun: null,
734
+ hidden: !!(sessionOpts && sessionOpts.hidden),
714
735
  terminalId: null,
715
736
  sessionOriginId: null,
716
737
  };
@@ -779,6 +800,7 @@ function createSessionManager(opts) {
779
800
  function switchSession(localId, targetWs, transform) {
780
801
  var session = sessions.get(localId);
781
802
  if (!session) return;
803
+ if (typeof onSessionHydrate === "function") onSessionHydrate(session);
782
804
 
783
805
  // Pane sockets (split-view iframes) pin their session via switch_session
784
806
  // but must not move the project-global active pointer: reloads and
@@ -806,7 +828,9 @@ function createSessionManager(opts) {
806
828
  var _vendorCaps = _capsByVendor[_sessionVendor] || {};
807
829
  _send({ type: "session_switched", id: localId, cliSessionId: session.cliSessionId || null, loop: session.loop || null, vendor: session.vendor || null, model: session.model || null, effort: session.effort || null, hasHistory: (session.history && session.history.length > 0), capabilities: _vendorCaps, isProcessing: !!session.isProcessing, mode: session.mode || "gui", terminalId: typeof session.terminalId === "number" ? session.terminalId : null, runtimeMode: session.runtimeMode || null, runtimeTerminalId: typeof session.runtimeTerminalId === "number" ? session.runtimeTerminalId : null, tuiSuspended: !!session.tuiSuspended, dangerouslySkipPermissions: !!session.dangerouslySkipPermissions, permissionMode: session.permissionMode || null, autonomousRun: projectAutonomousRun(session.autonomousRun), loopInterviewBrief: session.loopInterviewBrief || null, loopInterviewHandoff: session.loopInterviewHandoff || null, scheduledTaskDraft: session.scheduledTaskDraft || null });
808
830
  // Send vendor-specific slash commands
809
- var _vendorCmds = slashCommandsByVendor[_sessionVendor] || slashCommands || [];
831
+ var _vendorCmds = (session.slashCommandsByVendor && session.slashCommandsByVendor[_sessionVendor])
832
+ || session.slashCommands
833
+ || [];
810
834
  _send({ type: "slash_commands", commands: _vendorCmds, vendor: _sessionVendor });
811
835
  broadcastSessionList();
812
836
  replayHistory(session, undefined, targetWs, transform);
@@ -1076,6 +1100,8 @@ function createSessionManager(opts) {
1076
1100
  lastActivity: Date.now(),
1077
1101
  history: cliHistory,
1078
1102
  messageUUIDs: [],
1103
+ sessionVisibility: sessionVisibility.normalize(opts && opts.sessionVisibility),
1104
+ sessionVisibilityExplicit: !!(opts && opts.sessionVisibilityExplicit === true),
1079
1105
  bookmarked: false,
1080
1106
  favoriteOrder: null,
1081
1107
  };
@@ -1270,6 +1296,7 @@ function createSessionManager(opts) {
1270
1296
  }
1271
1297
 
1272
1298
  return {
1299
+ setSessionHydrator: function (fn) { onSessionHydrate = typeof fn === "function" ? fn : null; },
1273
1300
  get activeSessionId() { return activeSessionId; },
1274
1301
  get nextLocalId() { return nextLocalId; },
1275
1302
  get slashCommands() { return slashCommands; },
@@ -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 };