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

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 (42) hide show
  1. package/lib/notes.js +20 -3
  2. package/lib/os-users.js +6 -0
  3. package/lib/project-connection.js +10 -3
  4. package/lib/project-http.js +34 -37
  5. package/lib/project-sessions.js +3 -0
  6. package/lib/project-user-message.js +47 -0
  7. package/lib/project.js +22 -1
  8. package/lib/public/app.js +4 -0
  9. package/lib/public/css/issues.css +17 -3
  10. package/lib/public/css/sticky-notes.css +29 -0
  11. package/lib/public/modules/app-connection.js +6 -1
  12. package/lib/public/modules/app-messages.js +17 -4
  13. package/lib/public/modules/app-panels.js +21 -10
  14. package/lib/public/modules/app-projects.js +2 -0
  15. package/lib/public/modules/context-view-preference.js +143 -0
  16. package/lib/public/modules/pane-bridge.js +8 -8
  17. package/lib/public/modules/project-activation.js +49 -0
  18. package/lib/public/modules/scheduled-tasks.js +1 -1
  19. package/lib/public/modules/split-session-boundary.js +28 -0
  20. package/lib/public/modules/split-view.js +40 -7
  21. package/lib/public/modules/sticky-notes-browser.js +126 -5
  22. package/lib/public/modules/sticky-notes.js +2 -3
  23. package/lib/sdk-bridge.js +72 -61
  24. package/lib/sdk-message-processor.js +23 -10
  25. package/lib/sdk-skill-discovery.js +6 -28
  26. package/lib/server-context-view.js +53 -0
  27. package/lib/server.js +4 -0
  28. package/lib/session-title-generator.js +89 -0
  29. package/lib/session-title-policy.js +8 -2
  30. package/lib/sessions.js +22 -1
  31. package/lib/user-presence.js +6 -0
  32. package/lib/users-context-view-preferences.js +65 -0
  33. package/lib/users.js +6 -0
  34. package/lib/ws-schema.js +3 -0
  35. package/lib/yoke/adapters/acp.js +3 -2
  36. package/lib/yoke/adapters/antigravity.js +2 -2
  37. package/lib/yoke/adapters/claude.js +25 -7
  38. package/lib/yoke/adapters/codex.js +158 -16
  39. package/lib/yoke/adapters/kiro.js +6 -5
  40. package/lib/yoke/session-skill-catalog.js +45 -0
  41. package/lib/yoke/skill-discovery.js +60 -20
  42. package/package.json +1 -1
package/lib/notes.js CHANGED
@@ -39,8 +39,10 @@ function createNotesManager(opts) {
39
39
  var tmpPath = notesFile + ".tmp";
40
40
  fs.writeFileSync(tmpPath, JSON.stringify({ notes: notes }, null, 2));
41
41
  fs.renameSync(tmpPath, notesFile);
42
+ return true;
42
43
  } catch (e) {
43
44
  console.error("[notes] Failed to save:", e.message);
45
+ return false;
44
46
  }
45
47
  }
46
48
 
@@ -139,9 +141,23 @@ function createNotesManager(opts) {
139
141
  return lifecycle.closedNotes(notes);
140
142
  }
141
143
 
142
- // Permanent deletion. Deliberately not reachable from any WebSocket message,
143
- // MCP tool, or UI control: completion closes, it never erases. Retained only
144
- // for maintenance callers that must truly discard a record.
144
+ // Permanent deletion for the human-only closed-note path. Callers must still
145
+ // authorize the actor and recheck the lifecycle before invoking this method.
146
+ // The closed-state guard makes a stale confirmation harmless at storage.
147
+ function removeClosed(id) {
148
+ for (var i = 0; i < notes.length; i++) {
149
+ if (notes[i].id !== id || !lifecycle.isClosed(notes[i])) continue;
150
+ var removed = notes[i];
151
+ notes.splice(i, 1);
152
+ if (saveToDisk()) return removed;
153
+ notes.splice(i, 0, removed);
154
+ return null;
155
+ }
156
+ return null;
157
+ }
158
+
159
+ // Retained for maintenance callers that must truly discard a record. It is
160
+ // intentionally not exposed through WebSocket, MCP, or the UI.
145
161
  function remove(id) {
146
162
  for (var i = 0; i < notes.length; i++) {
147
163
  if (notes[i].id === id) {
@@ -197,6 +213,7 @@ function createNotesManager(opts) {
197
213
  close: close,
198
214
  reopen: reopen,
199
215
  remove: remove,
216
+ removeClosed: removeClosed,
200
217
  bringToFront: bringToFront,
201
218
  getActiveNotesText: getActiveNotesText,
202
219
  };
package/lib/os-users.js CHANGED
@@ -139,6 +139,12 @@ function fsAsUser(op, args, osUserInfo) {
139
139
  "var stat = fs.statSync(f);",
140
140
  "process.stdout.write(JSON.stringify({ size: stat.size, isDir: stat.isDirectory(), mtime: stat.mtimeMs }));",
141
141
  ].join(" ");
142
+ } else if (op === "realpath") {
143
+ script = [
144
+ "var fs = require('fs');",
145
+ "var f = " + JSON.stringify(args.file) + ";",
146
+ "process.stdout.write(JSON.stringify({ path: fs.realpathSync(f) }));",
147
+ ].join(" ");
142
148
  } else if (op === "read_binary") {
143
149
  // Read file as base64 for binary content (images, etc.)
144
150
  script = [
@@ -95,6 +95,7 @@ function attachConnection(ctx) {
95
95
  var getLatestVersion = ctx.getLatestVersion;
96
96
  var getTitle = ctx.getTitle;
97
97
  var getProject = ctx.getProject;
98
+ var hydrateSkillCatalog = ctx.hydrateSkillCatalog || function () { return []; };
98
99
  var warmup = ctx.warmup;
99
100
  var scheduledMessages = ctx.scheduledMessages;
100
101
  var sendCursorSharingState = ctx.sendCursorSharingState;
@@ -169,6 +170,7 @@ function attachConnection(ctx) {
169
170
  ? { active: null, storedPresence: null }
170
171
  : findRestoredActiveSession(ws, wsUser, allSessions);
171
172
  var restoredActive = restoredState.active;
173
+ if (restoredActive) hydrateSkillCatalog(restoredActive);
172
174
  var initialVendor = (restoredActive && restoredActive.vendor) || sm.defaultVendor || "claude";
173
175
  var initialEffort = (restoredActive && restoredActive.effort) || yoke.clampEffort(
174
176
  initialVendor,
@@ -184,8 +186,9 @@ function attachConnection(ctx) {
184
186
  // Update notifications are pushed on a scheduled interval (see
185
187
  // scheduleUpdateBroadcast). We no longer push on connect to avoid
186
188
  // re-triggering the banner on every page refresh.
187
- if (sm.slashCommands) {
188
- sendTo(ws, { type: "slash_commands", commands: sm.slashCommands });
189
+ var initialSlashCommands = restoredActive && (restoredActive.slashCommandsByVendor && restoredActive.slashCommandsByVendor[initialVendor] || restoredActive.slashCommands);
190
+ if (initialSlashCommands) {
191
+ sendTo(ws, { type: "slash_commands", commands: initialSlashCommands, vendor: initialVendor });
189
192
  }
190
193
  var initialModel = (restoredActive && restoredActive.model) || ((sm.defaultModelByVendor || {})[initialVendor]) || "";
191
194
  // Vendor installation state is needed even before a new project has a
@@ -346,7 +349,11 @@ function attachConnection(ctx) {
346
349
  var dcPresKey = ws._clayUser ? ws._clayUser.id : "_default";
347
350
  var dcExisting = userPresence.getPresence(slug, dcPresKey);
348
351
  var dcSession = sm.sessions.get(ws._clayActiveSession);
349
- userPresence.setPresence(slug, dcPresKey, userPresence.sessionIdForPersistence(dcSession), dcExisting ? dcExisting.mateDm : null);
352
+ var dcSessionId = userPresence.sessionIdForPersistence(dcSession);
353
+ var dcExistingSession = dcExisting && userPresence.findSession(sm.sessions, dcExisting.sessionId);
354
+ if (userPresence.shouldPersistDisconnectPresence(dcExisting, dcSessionId, dcExistingSession, dcSession)) {
355
+ userPresence.setPresence(slug, dcPresKey, dcSessionId, dcExisting ? dcExisting.mateDm : null);
356
+ }
350
357
  }
351
358
  tm.detachAll(ws);
352
359
  clients.delete(ws);
@@ -7,6 +7,7 @@ var defaultFsAsUser = require("./os-users").fsAsUser;
7
7
  var defaultUsersModule = require("./users");
8
8
  var gitCli = require("./git-cli");
9
9
  var resolveFilePath = require("./project-file-path").resolveFilePath;
10
+ var sharedSkillDiscovery = require("./yoke/skill-discovery");
10
11
 
11
12
  var IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ".ico"]);
12
13
  var MIME_TYPES = {
@@ -560,44 +561,40 @@ function attachHTTP(ctx) {
560
561
  // Installed skills (global + project)
561
562
  if (req.method === "GET" && urlPath === "/api/installed-skills") {
562
563
  var installed = {};
563
- var globalDir = path.join(require("./config").REAL_HOME, ".claude", "skills");
564
- var projectDir = path.join(cwd, ".claude", "skills");
565
- var scanDirs = [
566
- { dir: globalDir, scope: "global" },
567
- { dir: projectDir, scope: "project" },
568
- ];
569
- for (var sd = 0; sd < scanDirs.length; sd++) {
570
- var entries;
571
- try { entries = fs.readdirSync(scanDirs[sd].dir, { withFileTypes: true }); } catch (e) { continue; }
572
- for (var si = 0; si < entries.length; si++) {
573
- var ent = entries[si];
574
- if (!ent.isDirectory() && !ent.isSymbolicLink()) continue;
575
- var mdPath = path.join(scanDirs[sd].dir, ent.name, "SKILL.md");
576
- try {
577
- var mdContent = fs.readFileSync(mdPath, "utf8");
578
- var desc = "";
579
- // Parse YAML frontmatter for description
580
- var version = "";
581
- if (mdContent.startsWith("---")) {
582
- var endIdx = mdContent.indexOf("---", 3);
583
- if (endIdx !== -1) {
584
- var frontmatter = mdContent.substring(3, endIdx);
585
- var descMatch = frontmatter.match(/^description:\s*(.+)/m);
586
- if (descMatch) desc = descMatch[1].trim();
587
- var verMatch = frontmatter.match(/version:\s*"?([^"\n]+)"?/m);
588
- if (verMatch) version = verMatch[1].trim();
589
- }
590
- }
591
- if (!installed[ent.name]) {
592
- installed[ent.name] = { scope: scanDirs[sd].scope, description: desc, version: version, path: path.join(scanDirs[sd].dir, ent.name) };
593
- } else {
594
- // project-level adds to existing global entry
595
- installed[ent.name].scope = "both";
596
- if (desc && !installed[ent.name].description) installed[ent.name].description = desc;
597
- if (version && !installed[ent.name].version) installed[ent.name].version = version;
564
+ var installedUserInfo = getOsUserInfoForReq(req);
565
+ if (osUsers && !installedUserInfo) {
566
+ res.writeHead(403, { "Content-Type": "application/json" });
567
+ res.end('{"error":"A mapped OS user is required to list skills"}');
568
+ return true;
569
+ }
570
+ var installedOptions = installedUserInfo
571
+ ? sharedSkillDiscovery.optionsForOsUser(installedUserInfo, fsAsUser)
572
+ : { homeDir: require("./config").REAL_HOME };
573
+ installedOptions.includeDuplicates = true;
574
+ var discoveredInstalled = sharedSkillDiscovery.discoverSkills(cwd, installedOptions);
575
+ for (var si = 0; si < discoveredInstalled.length; si++) {
576
+ var discoveredSkill = discoveredInstalled[si];
577
+ try {
578
+ var desc = discoveredSkill.description || "";
579
+ var version = discoveredSkill.version || "";
580
+ var scope = discoveredSkill.source.indexOf("-project") !== -1 || discoveredSkill.source === "project" ? "project" : "global";
581
+ if (!installed[discoveredSkill.name]) {
582
+ installed[discoveredSkill.name] = { scope: scope, description: desc || discoveredSkill.description, version: version, path: path.dirname(discoveredSkill.path), source: discoveredSkill.source, sources: [discoveredSkill.source] };
583
+ } else {
584
+ // project-level adds to existing global entry
585
+ var priorScope = installed[discoveredSkill.name].scope;
586
+ installed[discoveredSkill.name].scope = priorScope === scope ? scope : "both";
587
+ if (installed[discoveredSkill.name].sources.indexOf(discoveredSkill.source) === -1) installed[discoveredSkill.name].sources.push(discoveredSkill.source);
588
+ if (scope === "project" || priorScope === "project") {
589
+ installed[discoveredSkill.name].path = path.dirname(discoveredSkill.path);
590
+ installed[discoveredSkill.name].source = discoveredSkill.source;
591
+ installed[discoveredSkill.name].description = desc || discoveredSkill.description;
592
+ installed[discoveredSkill.name].version = version;
598
593
  }
599
- } catch (e) {}
600
- }
594
+ if (desc && !installed[discoveredSkill.name].description) installed[discoveredSkill.name].description = desc;
595
+ if (version && !installed[discoveredSkill.name].version) installed[discoveredSkill.name].version = version;
596
+ }
597
+ } catch (e) {}
601
598
  }
602
599
  res.writeHead(200, { "Content-Type": "application/json" });
603
600
  res.end(JSON.stringify({ installed: installed }));
@@ -110,6 +110,7 @@ function attachSessions(ctx) {
110
110
  var getLinuxUserForSession = ctx.getLinuxUserForSession;
111
111
  var ensureProjectAccessForSession = ctx.ensureProjectAccessForSession;
112
112
  var getOsUserInfoForWs = ctx.getOsUserInfoForWs;
113
+ var hydrateSkillCatalog = ctx.hydrateSkillCatalog || function () { return []; };
113
114
  function stopSession(session, actor) {
114
115
  if (!session) return false;
115
116
  session._pendingMessageDrainPaused = true;
@@ -984,6 +985,8 @@ function attachSessions(ctx) {
984
985
  if (msg.id && sm.sessions.has(msg.id) && msg.title) {
985
986
  var s = sm.sessions.get(msg.id);
986
987
  s.title = String(msg.title).substring(0, 100);
988
+ s.titleProvisional = false;
989
+ s.titleAutoGenerated = false;
987
990
  s.titleManuallySet = true;
988
991
  sm.saveSessionFile(s);
989
992
  sm.notifySessionRenamed(s.localId);
@@ -24,6 +24,7 @@ var fs = require("fs");
24
24
  * pendingMessageQueue,
25
25
  * loadContextSources, saveContextSources,
26
26
  * digestDmTurn, gateMemory,
27
+ * getProjectAccess, canAccessProjectSlug,
27
28
  * adapter - YOKE adapter instance
28
29
  */
29
30
  function attachUserMessage(ctx) {
@@ -55,6 +56,8 @@ function attachUserMessage(ctx) {
55
56
  var getLinuxUserForSession = ctx.getLinuxUserForSession;
56
57
  var ensureProjectAccessForSession = ctx.ensureProjectAccessForSession;
57
58
  var getOsUserInfoForWs = ctx.getOsUserInfoForWs;
59
+ var getProjectAccess = ctx.getProjectAccess;
60
+ var canAccessProjectSlug = ctx.canAccessProjectSlug;
58
61
 
59
62
  var hydrateImageRefs = ctx.hydrateImageRefs;
60
63
  var saveImageFile = ctx.saveImageFile;
@@ -99,6 +102,21 @@ function attachUserMessage(ctx) {
99
102
  }
100
103
  }
101
104
 
105
+ function canHumanManageNotes(ws) {
106
+ if (!usersModule.isMultiUser()) return true;
107
+ if (!ws || !ws._clayUser || !ws._clayUser.id) return false;
108
+ var actor = usersModule.findUserById(ws._clayUser.id);
109
+ if (!actor) return false;
110
+ if (typeof canAccessProjectSlug === "function" && !canAccessProjectSlug(actor.id, slug)) return false;
111
+ var access = typeof getProjectAccess === "function" ? getProjectAccess() : null;
112
+ return !!(access && !access.error && usersModule.canAccessProject(actor.id, access));
113
+ }
114
+
115
+ function noteDeleteResult(ws, msg, ok, error) {
116
+ sendTo(ws, { type: "note_delete_result", projectSlug: slug, noteId: msg.id || null,
117
+ requestId: msg.requestId || null, ok: !!ok, error: error || null });
118
+ }
119
+
102
120
  // --------------- Main handler ---------------
103
121
 
104
122
  function handleUserMessage(ws, msg, forcedSession, internal) {
@@ -171,6 +189,31 @@ function attachUserMessage(ctx) {
171
189
  return true;
172
190
  }
173
191
 
192
+ if (msg.type === "note_delete_permanently") {
193
+ if (!msg.id || !canHumanManageNotes(ws)) {
194
+ noteDeleteResult(ws, msg, false, "You are not authorized to delete notes in this project.");
195
+ return true;
196
+ }
197
+ var current = nm.list().filter(function (item) { return item && item.id === msg.id; })[0];
198
+ if (!current) {
199
+ noteDeleteResult(ws, msg, false, "Note not found.");
200
+ return true;
201
+ }
202
+ if (!notesLifecycle.isClosed(current)) {
203
+ noteDeleteResult(ws, msg, false, "Only closed notes can be permanently deleted.");
204
+ return true;
205
+ }
206
+ var removed = nm.removeClosed(msg.id);
207
+ if (!removed) {
208
+ noteDeleteResult(ws, msg, false, "The note changed before it could be deleted.");
209
+ return true;
210
+ }
211
+ send({ type: "note_deleted", id: removed.id });
212
+ noteDeleteResult(ws, msg, true, null);
213
+ syncNotesKnowledge();
214
+ return true;
215
+ }
216
+
174
217
  if (msg.type === "note_list_request") {
175
218
  sendTo(ws, { type: "notes_list", notes: nm.list() });
176
219
  return true;
@@ -479,6 +522,10 @@ function attachUserMessage(ctx) {
479
522
  function recordSessionTitle() {
480
523
  if (!session.title) {
481
524
  session.title = (msg.text || "Image").substring(0, 50);
525
+ // This title is an optimistic first-message label, not a semantic or
526
+ // explicit title. Persist its provenance so startQuery can preserve
527
+ // eligibility for model generation after this function returns.
528
+ session.titleProvisional = true;
482
529
  sm.saveSessionFile(session);
483
530
  sm.broadcastSessionList();
484
531
  // Sync auto-title to SDK
package/lib/project.js CHANGED
@@ -46,6 +46,7 @@ var { attachVendorLogin } = require("./project-vendor-login");
46
46
  var { attachConnection } = require("./project-connection");
47
47
  var { attachProjectScheduledMessages } = require("./project-scheduled-messages");
48
48
  var { attachMcp } = require("./project-mcp");
49
+ var { createSessionSkillCatalog } = require("./yoke/session-skill-catalog");
49
50
  var { createLocalMcp } = require("./mcp-local");
50
51
  var { attachEmail: attachEmailModule } = require("./project-email");
51
52
  var { attachSessionSpawn } = require("./project-session-spawn");
@@ -1598,7 +1599,7 @@ function createProjectContext(opts) {
1598
1599
  }
1599
1600
 
1600
1601
  // --- 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") {
1602
+ 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
1603
  if (typeof opts.onDmMessage === "function") opts.onDmMessage(ws, msg, slug);
1603
1604
  return;
1604
1605
  }
@@ -1878,6 +1879,12 @@ function createProjectContext(opts) {
1878
1879
  }
1879
1880
 
1880
1881
  // --- Sessions/config/project handler (delegated to project-sessions.js) ---
1882
+ var _sessionSkillCatalog = createSessionSkillCatalog({
1883
+ cwd: cwd,
1884
+ osUsers: osUsers,
1885
+ getLinuxUser: getLinuxUserForSession,
1886
+ });
1887
+ sm.setSessionHydrator(_sessionSkillCatalog.hydrate);
1881
1888
  var _sessions = attachSessions({
1882
1889
  cwd: cwd,
1883
1890
  slug: slug,
@@ -1917,6 +1924,7 @@ function createProjectContext(opts) {
1917
1924
  getLinuxUserForSession: getLinuxUserForSession,
1918
1925
  ensureProjectAccessForSession: ensureProjectAccessForSession,
1919
1926
  getOsUserInfoForWs: getOsUserInfoForWs,
1927
+ hydrateSkillCatalog: _sessionSkillCatalog.hydrate,
1920
1928
  hydrateImageRefs: hydrateImageRefs,
1921
1929
  onProcessingChanged: onProcessingChanged,
1922
1930
  broadcastPresence: broadcastPresence,
@@ -2046,6 +2054,8 @@ function createProjectContext(opts) {
2046
2054
  getSessionForWs: getSessionForWs,
2047
2055
  getLinuxUserForSession: getLinuxUserForSession,
2048
2056
  ensureProjectAccessForSession: ensureProjectAccessForSession,
2057
+ getProjectAccess: function () { return opts.getProjectAccess ? opts.getProjectAccess() : { visibility: "public", ownerId: projectOwnerId || null }; },
2058
+ canAccessProjectSlug: opts.canAccessProjectSlug || function () { return !usersModule.isMultiUser(); },
2049
2059
  getOsUserInfoForWs: getOsUserInfoForWs,
2050
2060
  hydrateImageRefs: hydrateImageRefs,
2051
2061
  saveImageFile: saveImageFile,
@@ -2128,6 +2138,12 @@ function createProjectContext(opts) {
2128
2138
  function getMcpBridgeHandler(sessionId, sessionOnly, queryGeneration) {
2129
2139
  var boundSession = Number.isInteger(sessionId) ? sm.sessions.get(sessionId) : null;
2130
2140
  var queryToolBridge = createSessionQueryToolBridge({ session: boundSession, sdk: sdk });
2141
+ function sessionQueryValid() {
2142
+ return !!boundSession
2143
+ && Number.isInteger(queryGeneration)
2144
+ && sm.sessions.get(boundSession.localId) === boundSession
2145
+ && Number(boundSession._sdkQueryGeneration || 0) === queryGeneration;
2146
+ }
2131
2147
  // Build set of local MCP server names to exclude (Codex handles these natively)
2132
2148
  var localMcpNames = {};
2133
2149
  try {
@@ -2142,6 +2158,7 @@ function createProjectContext(opts) {
2142
2158
  return {
2143
2159
  listTools: function () {
2144
2160
  var tools = [];
2161
+ if (sessionOnly && !sessionQueryValid()) return Promise.resolve(tools);
2145
2162
  var toJSONSchema;
2146
2163
  var zod;
2147
2164
  try { zod = require("zod"); toJSONSchema = zod.toJSONSchema; } catch (e) { /* fallback */ }
@@ -2267,6 +2284,9 @@ function createProjectContext(opts) {
2267
2284
  return Promise.resolve(tools);
2268
2285
  },
2269
2286
  callTool: function (serverName, toolName, args) {
2287
+ if (sessionOnly && !sessionQueryValid()) {
2288
+ return Promise.reject(new Error("Session tool unavailable or older query: " + serverName + "/" + toolName));
2289
+ }
2270
2290
  var queryCall = queryToolBridge.callTool(queryGeneration, serverName, toolName, args);
2271
2291
  if (queryCall) return queryCall;
2272
2292
  if (boundSession && serverName === "clay-sessions") {
@@ -2425,6 +2445,7 @@ function createProjectContext(opts) {
2425
2445
  getLatestVersion: function () { return latestVersion; },
2426
2446
  getTitle: function () { return title; },
2427
2447
  getProject: function () { return project; },
2448
+ hydrateSkillCatalog: _sessionSkillCatalog.hydrate,
2428
2449
  // Exposed so the first websocket connection can lazily warm up the
2429
2450
  // adapters for this project (see project-connection handleConnection).
2430
2451
  warmup: function (linuxUser) {
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: {},
@@ -1,7 +1,20 @@
1
- #issues-panel { position: relative; width: 48%; max-width: 760px; min-width: 360px; height: calc(100% - 16px); margin: 8px; align-self: center; display: flex; flex-direction: column; background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; flex-shrink: 0; }
1
+ #issues-panel { position: relative; width: 50%; max-width: 720px; min-width: 360px; height: calc(100% - 16px); margin: 8px 8px 8px 10px; min-height: 0; align-self: center; display: flex; flex-direction: column; background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; flex-shrink: 0; }
2
+ @media (min-width: 1024px) {
3
+ #issues-panel {
4
+ box-shadow:
5
+ 4px 8px 18px rgba(var(--shadow-rgb), 0.14),
6
+ 2px 2px 4px rgba(var(--shadow-rgb), 0.08),
7
+ inset 0 1px 0 rgba(255, 255, 255, 0.04);
8
+ animation: workbench-panel-in 0.18s cubic-bezier(0.2, 0.8, 0.2, 1);
9
+ transition: max-width 0.2s ease, width 0.2s ease, box-shadow 0.2s ease;
10
+ }
11
+ }
12
+ @media (prefers-reduced-motion: reduce) {
13
+ #issues-panel { animation: none; }
14
+ }
2
15
  #issues-panel.hidden { display: none; }
3
16
  #issues-panel.issues-wide { width: 70%; max-width: 1200px; }
4
- #issues-panel.panel-fullscreen { width: 100%; max-width: none; height: 100%; margin: 0; }
17
+ #issues-panel.panel-fullscreen { width: 100%; max-width: none; min-width: 0; height: 100%; margin: 0; border: 0; border-radius: 0; box-shadow: none; animation: none; }
5
18
  #issues-panel header { height: 48px; box-sizing: border-box; display: flex; align-items: center; gap: 12px; padding: 0 16px; border-bottom: 1px solid var(--border-subtle); flex: 0 0 auto; }
6
19
  #issues-panel .issues-title { display: inline-flex; align-items: center; gap: 7px; font-weight: 700; }
7
20
  #issues-panel .issues-title .lucide { width: 15px; color: var(--accent); }
@@ -79,7 +92,8 @@
79
92
  .issues-content > h2 { margin: 0 0 10px; font-family: var(--font-display); font-size: 14px; line-height: 1.2; }
80
93
  .issues-history-note { margin: 0; padding: 14px 18px; border-bottom: 1px solid var(--border-subtle); color: var(--text-dimmer); font-size: 11px; font-family: var(--font-mono); }
81
94
  .clayos-issue-link { display: inline-flex; max-width: 100%; border: 1px solid var(--border); border-radius: 5px; background: var(--bg); color: inherit; font: inherit; font-size: .85em; padding: 2px 7px; cursor: pointer; vertical-align: baseline; overflow-wrap: anywhere; }
82
- @media (max-width: 1023px) { #issues-panel, #issues-panel.issues-wide { position: fixed; inset: 0; z-index: 150; width: 100%; max-width: none; min-width: 0; height: 100%; margin: 0; border-radius: 0; padding-top: var(--safe-top, 0px); padding-bottom: var(--safe-bottom, 0px); box-sizing: border-box; } #issues-panel button { min-height: 36px; } }
95
+ @media (max-width: 1023px) { #issues-panel, #issues-panel.issues-wide { position: fixed; inset: 0; z-index: 300; width: 100%; max-width: none; min-width: 0; height: 100%; margin: 0; border: 0; border-radius: 0; padding-top: var(--safe-top, 0px); padding-bottom: var(--safe-bottom, 0px); box-sizing: border-box; } #issues-panel button { min-height: 36px; } }
96
+ @media (max-width: 1023px) { #issues-panel [data-issues-wide], #issues-panel [data-issues-full] { display: none; } }
83
97
  @media (max-width: 390px) {
84
98
  #issues-panel header { gap: 4px; padding: 8px; }
85
99
  #issues-panel header button { padding: 7px; }
@@ -764,6 +764,35 @@
764
764
  .notes-browser-card-action:hover { background: var(--bg-alt); color: var(--text); }
765
765
  .notes-browser-card-action:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
766
766
 
767
+ .notes-delete-dialog {
768
+ position: absolute;
769
+ inset: 0;
770
+ z-index: 2;
771
+ display: flex;
772
+ align-items: center;
773
+ justify-content: center;
774
+ padding: 24px;
775
+ background: rgba(var(--shadow-rgb), 0.28);
776
+ }
777
+ .notes-delete-dialog.hidden { display: none; }
778
+ .notes-delete-dialog-card {
779
+ width: min(420px, 100%);
780
+ padding: 20px;
781
+ border: 1px solid var(--border);
782
+ border-radius: 10px;
783
+ background: var(--bg);
784
+ box-shadow: 0 12px 36px rgba(var(--shadow-rgb), 0.24);
785
+ }
786
+ .notes-delete-dialog-card h2 { margin: 0 0 10px; font-size: 16px; }
787
+ .notes-delete-dialog-card p { margin: 8px 0; color: var(--text-secondary); font-size: 12px; }
788
+ .notes-delete-dialog-title { font-weight: 700; color: var(--text) !important; overflow-wrap: anywhere; }
789
+ .notes-delete-dialog-status { min-height: 18px; }
790
+ .notes-delete-dialog-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
791
+ .notes-delete-dialog-actions button { padding: 7px 12px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--text); font: inherit; cursor: pointer; }
792
+ .notes-delete-dialog-actions button.danger { background: var(--danger, #b42318); border-color: var(--danger, #b42318); color: #fff; }
793
+ .notes-delete-dialog-actions button:disabled { opacity: 0.55; cursor: default; }
794
+ .notes-delete-dialog-actions button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
795
+
767
796
  .notes-browser-card-body {
768
797
  flex: 1;
769
798
  font-size: 11.5px;
@@ -17,7 +17,9 @@ import { resumeHomeChat } from './home-mate-chat.js';
17
17
  import { requestHomeSurfacePreference } from './home-surface.js';
18
18
  import { isHomeDebatesSurface } from './home-sub-surface.js';
19
19
  import { beginDefaultAiConnection, requestDefaultAi } from './default-ai.js';
20
+ import { beginContextViewConnection, requestContextView } from './context-view-preference.js';
20
21
  import { beginDefaultVendorConnection, requestDefaultVendor } from './default-vendor.js';
22
+ import { clearProjectSplitState } from './split-session-boundary.js';
21
23
 
22
24
  var reconnectTimer = null;
23
25
  var reconnectDelay = 1000;
@@ -234,7 +236,8 @@ export function connect() {
234
236
 
235
237
  var protocol = location.protocol === "https:" ? "wss:" : "ws:";
236
238
  var socketPath = store.get('wsPath');
237
- store.set({ socketPath: socketPath, activeProjectSlug: null, sessionActivatedProjectSlug: null });
239
+ if (store.get('socketPath') && store.get('socketPath') !== socketPath) clearProjectSplitState();
240
+ store.set({ socketPath: socketPath, activeProjectSlug: null, sessionActivatedProjectSlug: null, sessionListProjectSlug: null, splitGroupsProjectSlug: null });
238
241
  var newWs = new WebSocket(protocol + "//" + location.host + socketPath);
239
242
  setWs(newWs);
240
243
 
@@ -280,6 +283,8 @@ export function connect() {
280
283
  requestDefaultAi();
281
284
  beginDefaultVendorConnection();
282
285
  requestDefaultVendor();
286
+ beginContextViewConnection();
287
+ requestContextView();
283
288
  };
284
289
 
285
290
  newWs.onclose = function (e) {
@@ -20,6 +20,7 @@ import { renderMateSessionList, handleMateSearchResults, updateMateSidebarProfil
20
20
  import { openHomeChat, handleHomeMateHistory, handleHomeMateDelta, handleHomeMateSegment, handleHomeMateDone, handleHomeMateError, handleHomeMateSessionsState } from './home-mate-chat.js';
21
21
  import { handleHomeSurfaceState } from './home-surface.js';
22
22
  import { handleDefaultVendorMessage } from './default-vendor.js';
23
+ import { handleContextViewMessage } from './context-view-preference.js';
23
24
  import { handleHomeMateMemoryState, handleHomeMateKnowledgeState } from './home-mate-settings.js';
24
25
  import { renderKnowledgeList, handleKnowledgeContent } from './mate-knowledge.js';
25
26
  import { renderMemoryList } from './mate-memory.js';
@@ -45,6 +46,7 @@ import { tuiModalHandleTermOutput, tuiModalHandleTermResized, tuiModalHandleTerm
45
46
  import { updateTerminalList, handleContextSourcesState, updateEmailAccountList, updateEmailUnreadCounts, handleEmailTestResult, handleEmailAddResult, handleEmailRemoveResult } from './context-sources.js';
46
47
  import { refreshEmailSettings } from './user-settings.js';
47
48
  import { handleNotesList, handleNoteCreated, handleNoteUpdated, handleNoteDeleted, handleNoteWritten } from './sticky-notes.js';
49
+ import { handleNoteDeleteResult } from './sticky-notes-browser.js';
48
50
  import { handleProjectLogsState, handleProjectLogEntry, handleProjectLogCommented, handleProjectLogCommentReviewed, handleProjectLogDeleted, handleProjectLogUpdated, handleProjectLogsError, handleScheduledTaskResultsState, handleScheduledTaskResultCreated, handleScheduledTaskResultSession } from './project-logs.js';
49
51
  import { handleSkillInstalled, handleSkillUninstalled } from './skills.js';
50
52
  import { showRewindModal, onRewindComplete, setRewindMode, onRewindError, clearPendingRewindUuid, addRewindButton } from './rewind.js';
@@ -64,6 +66,7 @@ import { setStatus } from './app-connection.js';
64
66
  import { handleWhatsNewState, handleWhatsNewSeenResult, setKnownEntries as setWhatsNewKnownEntries } from './whats-new.js';
65
67
  import { closeArticle as closeWhatsNewArticle } from './whats-new-article.js';
66
68
  import { resolvePaneSession, resolveSwitchedVendor } from './pane-session.js';
69
+ import { markProjectSessionListHydrated, applyProjectSplitGroups, clearProjectSplitState } from './split-session-boundary.js';
67
70
  import { selectDefaultVendorForBlankSession } from './vendor-selection.js';
68
71
  import { getModelInfoUpdate, modelEntryValue, modelEntryMatches, handleModelSelectionResult, requestVendorModels } from './model-picker.js';
69
72
  import { getModelEffortLevels, accumulateUsage, updateUsagePanel, accumulateContext, updateContextPanel, renderCtxPopover, updateStatusPanel, markContextUnavailable } from './app-panels.js';
@@ -99,6 +102,7 @@ export function processMessage(msg) {
99
102
  if (handleLoopInterviewMessage(msg)) return;
100
103
  if (handleScheduledTaskMessage(msg)) return;
101
104
  if (handleDefaultVendorMessage(msg)) return;
105
+ if (handleContextViewMessage(msg)) return;
102
106
  if (msg && msg.type === "schedule_message_result") {
103
107
  handleScheduleMessageResult(msg);
104
108
  return;
@@ -331,10 +335,13 @@ export function processMessage(msg) {
331
335
  // start receiving the new project's session_switched. The TUI
332
336
  // host lives on document.body (it's position: fixed), so it
333
337
  // survives project navigation unless we detach explicitly here.
338
+ if (msg.slug && store.get('activeProjectSlug') && store.get('activeProjectSlug') !== msg.slug) {
339
+ clearProjectSplitState();
340
+ }
334
341
  detachTuiView();
335
342
  store.set({ projectName: msg.project || msg.cwd, vendorInfo: msg.vendors || {} });
336
343
  if (msg.cwd) store.set({ cwd: msg.cwd });
337
- if (msg.slug) store.set({ currentSlug: msg.slug, activeProjectSlug: msg.slug });
344
+ if (msg.slug) store.set({ currentSlug: msg.slug, activeProjectSlug: msg.slug, sessionListProjectSlug: null, splitGroupsProjectSlug: null });
338
345
  try { var _is = store.snap(); localStorage.setItem("clay-project-name-" + (_is.currentSlug || "default"), _is.projectName); } catch (e) {}
339
346
  // In mate DM, keep title as mate name and re-apply mate color
340
347
  if (store.get('dmMode') && store.get('dmTargetUser') && store.get('dmTargetUser').isMate) {
@@ -691,6 +698,7 @@ export function processMessage(msg) {
691
698
  break;
692
699
 
693
700
  case "session_list":
701
+ markProjectSessionListHydrated();
694
702
  renderMateSessionList(msg.sessions || []);
695
703
  renderSessionList(msg.sessions || []);
696
704
  syncPaneTitles();
@@ -702,13 +710,14 @@ export function processMessage(msg) {
702
710
  }
703
711
  }
704
712
  handlePaletteSessionSwitch();
713
+ maybeRestoreSplitGroup();
705
714
  break;
706
715
 
707
716
  case "split_groups":
708
- store.set({ splitGroups: msg.groups || [] });
717
+ applyProjectSplitGroups(msg.groups || []);
709
718
  renderSessionList(null);
710
- maybeRestoreSplitGroup();
711
719
  syncPaneTitles();
720
+ maybeRestoreSplitGroup();
712
721
  break;
713
722
 
714
723
  case "split_group_result":
@@ -889,8 +898,8 @@ export function processMessage(msg) {
889
898
  }
890
899
  // Reload survival: a restored active session that is a split-group
891
900
  // member reopens its group (no-op when a split is already open).
892
- maybeRestoreSplitGroup();
893
901
  if (finishProjectSessionActivation()) hideHomeHub();
902
+ maybeRestoreSplitGroup();
894
903
  break;
895
904
 
896
905
  case "session_full_access_changed":
@@ -1638,6 +1647,10 @@ export function processMessage(msg) {
1638
1647
  handleNoteDeleted(msg);
1639
1648
  break;
1640
1649
 
1650
+ case "note_delete_result":
1651
+ handleNoteDeleteResult(msg);
1652
+ break;
1653
+
1641
1654
  case "project_logs_state":
1642
1655
  handleProjectLogsState(msg);
1643
1656
  break;