clay-server 3.0.0 → 3.1.0-beta.2

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 (35) hide show
  1. package/lib/mates-prompts.js +3 -2
  2. package/lib/notes.js +6 -0
  3. package/lib/project-http.js +3 -6
  4. package/lib/project-session-notes.js +217 -0
  5. package/lib/project.js +84 -18
  6. package/lib/public/app.js +1 -4
  7. package/lib/public/css/icon-strip.css +190 -0
  8. package/lib/public/css/messages.css +104 -2
  9. package/lib/public/css/mobile-nav.css +0 -4
  10. package/lib/public/css/pane.css +27 -0
  11. package/lib/public/css/rewind.css +10 -0
  12. package/lib/public/css/sticky-notes.css +43 -0
  13. package/lib/public/css/title-bar.css +0 -169
  14. package/lib/public/index.html +0 -6
  15. package/lib/public/modules/app-favicon.js +10 -1
  16. package/lib/public/modules/app-home-hub.js +0 -2
  17. package/lib/public/modules/app-messages.js +18 -6
  18. package/lib/public/modules/app-projects.js +30 -43
  19. package/lib/public/modules/app-rendering.js +42 -3
  20. package/lib/public/modules/dom-refs.js +1 -0
  21. package/lib/public/modules/input.js +13 -0
  22. package/lib/public/modules/project-switcher.js +1 -2
  23. package/lib/public/modules/sidebar-mobile.js +6 -19
  24. package/lib/public/modules/sidebar-projects.js +232 -63
  25. package/lib/public/modules/split-pair-ui.js +30 -0
  26. package/lib/public/modules/sticky-note-markdown.js +89 -0
  27. package/lib/public/modules/sticky-notes.js +56 -103
  28. package/lib/sdk-bridge.js +25 -1
  29. package/lib/session-notes-mcp-server.js +39 -0
  30. package/lib/ws-schema.js +1 -0
  31. package/lib/yoke/adapters/codex.js +44 -8
  32. package/lib/yoke/mcp-bridge-server.js +7 -7
  33. package/package.json +1 -1
  34. package/lib/public/modules/branch-switcher.js +0 -178
  35. package/lib/public/modules/worktree-family.js +0 -60
@@ -1,4 +1,6 @@
1
1
  import { refreshIcons, iconHtml } from './icons.js';
2
+ import { VENDOR_AVATARS } from './app-rendering.js';
3
+ import { extractMarkdown, getTitle, renderMiniMarkdown } from './sticky-note-markdown.js';
2
4
 
3
5
  var ctx;
4
6
  var notes = new Map(); // id -> { data, el }
@@ -135,106 +137,11 @@ function debouncedTextUpdate(id, text) {
135
137
  }, 500);
136
138
  }
137
139
 
138
- // --- Simple markdown ---
139
-
140
- function getTitle(text) {
141
- if (!text) return "";
142
- var idx = text.indexOf("\n");
143
- return idx === -1 ? text : text.substring(0, idx);
144
- }
145
-
146
- function renderMiniMarkdown(text) {
147
- if (!text) return "";
148
- var lines = text.split("\n");
149
- var title = lines[0];
150
- var body = lines.slice(1).join("\n");
151
-
152
- function fmt(s) {
153
- var escaped = s
154
- .replace(/&/g, "&")
155
- .replace(/</g, "&lt;")
156
- .replace(/>/g, "&gt;");
157
- return escaped
158
- .replace(/(https?:\/\/[^\s<]+)/g, '<a href="$1" target="_blank" rel="noopener">$1</a>')
159
- .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
160
- .replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, "<em>$1</em>")
161
- .replace(/`([^`]+)`/g, "<code>$1</code>")
162
- .replace(/~~(.+?)~~/g, "<del>$1</del>")
163
- .replace(/^- \[x\]/gm, '<span class="sn-check checked">✓</span>')
164
- .replace(/^- \[ \]/gm, '<span class="sn-check">☐</span>')
165
- .replace(/\n/g, "<br>");
166
- }
167
-
168
- var html = '<div class="sn-title">' + fmt(title) + '</div>';
169
- if (body.trim()) {
170
- html += fmt(body);
171
- }
172
- return html;
173
- }
174
-
175
140
  function syncTitle(noteEl, text) {
176
141
  var spacer = noteEl.querySelector(".sticky-note-spacer");
177
142
  if (spacer) spacer.textContent = getTitle(text) || "Untitled";
178
143
  }
179
144
 
180
- // --- HTML-to-Markdown reverse conversion (for contenteditable) ---
181
-
182
- function nodeToMd(node) {
183
- if (node.nodeType === 3) return node.textContent;
184
- if (node.nodeType !== 1) return "";
185
-
186
- var tag = node.tagName;
187
- var inner = childrenToMd(node);
188
-
189
- switch (tag) {
190
- case "STRONG": case "B": return "**" + inner + "**";
191
- case "EM": case "I": return "*" + inner + "*";
192
- case "DEL": case "S": case "STRIKE": return "~~" + inner + "~~";
193
- case "CODE": return "`" + inner + "`";
194
- case "BR": return "\n";
195
- case "DIV":
196
- if (node.classList.contains("sn-title")) return inner;
197
- if (node.classList.contains("sn-placeholder")) return "";
198
- // Browser-generated div from Enter key = new line
199
- return "\n" + inner;
200
- case "P": return "\n" + inner;
201
- case "A": return node.getAttribute("href") || inner;
202
- case "SPAN":
203
- if (node.classList.contains("sn-check")) {
204
- return node.classList.contains("checked") ? "- [x]" : "- [ ]";
205
- }
206
- if (node.classList.contains("sn-placeholder")) return "";
207
- return inner;
208
- default: return inner;
209
- }
210
- }
211
-
212
- function childrenToMd(el) {
213
- var result = "";
214
- for (var i = 0; i < el.childNodes.length; i++) {
215
- result += nodeToMd(el.childNodes[i]);
216
- }
217
- return result;
218
- }
219
-
220
- function extractMdFromRendered(rendered) {
221
- var titleEl = rendered.querySelector(".sn-title");
222
- if (titleEl) {
223
- var titleMd = childrenToMd(titleEl);
224
- var rest = "";
225
- var afterTitle = false;
226
- for (var i = 0; i < rendered.childNodes.length; i++) {
227
- var child = rendered.childNodes[i];
228
- if (child === titleEl) { afterTitle = true; continue; }
229
- if (afterTitle) rest += nodeToMd(child);
230
- }
231
- if (rest && rest.charAt(0) === "\n") rest = rest.substring(1);
232
- return titleMd + (rest ? "\n" + rest : "");
233
- }
234
- var md = childrenToMd(rendered);
235
- return md.replace(/^\n+/, "");
236
- }
237
-
238
145
  // --- Note rendering ---
239
146
 
240
147
  function renderNote(data) {
@@ -256,6 +163,15 @@ function renderNote(data) {
256
163
  var header = document.createElement("div");
257
164
  header.className = "sticky-note-header";
258
165
 
166
+ if (data.origin && VENDOR_AVATARS[data.origin.vendor]) {
167
+ var originBadge = document.createElement("img");
168
+ originBadge.className = "sticky-note-agent-badge";
169
+ originBadge.src = VENDOR_AVATARS[data.origin.vendor];
170
+ originBadge.alt = "";
171
+ originBadge.title = "Created by " + data.origin.vendor;
172
+ header.appendChild(originBadge);
173
+ }
174
+
259
175
  var closeBtn = document.createElement("button");
260
176
  closeBtn.className = "sticky-note-btn sticky-note-close";
261
177
  closeBtn.title = "Close";
@@ -603,13 +519,29 @@ function setupTextEdit(textarea, rendered, noteId, mdBtn) {
603
519
  var noteEl = textarea.closest(".sticky-note");
604
520
  var mdMode = false;
605
521
 
522
+ function saveRenderedMarkdown() {
523
+ var markdown = extractMarkdown(rendered);
524
+ textarea.value = markdown;
525
+ debouncedTextUpdate(noteId, markdown);
526
+ syncTitle(noteEl, markdown);
527
+ rendered.classList.toggle("is-empty", !markdown.trim());
528
+ }
529
+
530
+ function toggleChecklistItem(check) {
531
+ var checked = !check.classList.contains("checked");
532
+ check.classList.toggle("checked", checked);
533
+ check.textContent = checked ? "✓" : "☐";
534
+ check.setAttribute("aria-checked", checked ? "true" : "false");
535
+ saveRenderedMarkdown();
536
+ }
537
+
606
538
  // MD button: toggle between contenteditable rendered view and raw textarea
607
539
  mdBtn.addEventListener("click", function (e) {
608
540
  e.stopPropagation();
609
541
  mdMode = !mdMode;
610
542
  if (mdMode) {
611
543
  // Switch to raw markdown editing
612
- var md = extractMdFromRendered(rendered);
544
+ var md = extractMarkdown(rendered);
613
545
  textarea.value = md;
614
546
  textarea.style.display = "";
615
547
  rendered.style.display = "none";
@@ -636,11 +568,15 @@ function setupTextEdit(textarea, rendered, noteId, mdBtn) {
636
568
 
637
569
  // Sync contenteditable changes to textarea (data store) and save
638
570
  rendered.addEventListener("input", function () {
639
- var md = extractMdFromRendered(rendered);
640
- textarea.value = md;
641
- debouncedTextUpdate(noteId, md);
642
- syncTitle(noteEl, md);
643
- rendered.classList.toggle("is-empty", !md.trim());
571
+ saveRenderedMarkdown();
572
+ });
573
+
574
+ rendered.addEventListener("click", function (e) {
575
+ var check = e.target.closest && e.target.closest(".sn-check");
576
+ if (!check || !rendered.contains(check)) return;
577
+ e.preventDefault();
578
+ e.stopPropagation();
579
+ toggleChecklistItem(check);
644
580
  });
645
581
 
646
582
  // On blur, re-render to normalize HTML structure
@@ -648,7 +584,7 @@ function setupTextEdit(textarea, rendered, noteId, mdBtn) {
648
584
  // Don't re-render if clicking format toolbar (it prevents default, but just in case)
649
585
  if (e.relatedTarget && e.relatedTarget.closest && e.relatedTarget.closest(".sn-format-toolbar")) return;
650
586
  closeFormatToolbar();
651
- var md = extractMdFromRendered(rendered);
587
+ var md = extractMarkdown(rendered);
652
588
  textarea.value = md;
653
589
  if (md.trim()) {
654
590
  rendered.innerHTML = renderMiniMarkdown(md);
@@ -686,6 +622,12 @@ function setupTextEdit(textarea, rendered, noteId, mdBtn) {
686
622
 
687
623
  // Insert <br> on Enter instead of <div>
688
624
  rendered.addEventListener("keydown", function (e) {
625
+ if (e.target.classList && e.target.classList.contains("sn-check") &&
626
+ (e.key === "Enter" || e.key === " ")) {
627
+ e.preventDefault();
628
+ toggleChecklistItem(e.target);
629
+ return;
630
+ }
689
631
  if (e.key === "Enter" && !e.shiftKey) {
690
632
  e.preventDefault();
691
633
  document.execCommand("insertLineBreak");
@@ -916,6 +858,17 @@ export function handleNoteUpdated(msg) {
916
858
  if (archiveOpen) renderArchiveCards();
917
859
  }
918
860
 
861
+ export function handleNoteWritten(msg) {
862
+ var entry = notes.get(msg.id);
863
+ if (!entry) return;
864
+ entry.el.classList.remove("sticky-note-attention");
865
+ void entry.el.offsetWidth;
866
+ entry.el.classList.add("sticky-note-attention");
867
+ setTimeout(function () {
868
+ entry.el.classList.remove("sticky-note-attention");
869
+ }, 2100);
870
+ }
871
+
919
872
  export function handleNoteDeleted(msg) {
920
873
  var entry = notes.get(msg.id);
921
874
  if (!entry) return;
@@ -958,7 +911,7 @@ function renderArchiveCards() {
958
911
  if (notes.size === 0) {
959
912
  var empty = document.createElement("div");
960
913
  empty.className = "notes-archive-empty";
961
- empty.innerHTML = iconHtml("sticky-note") + "<p>No sticky notes yet</p><p class=\"notes-archive-empty-sub\">Create one with the " + iconHtml("sticky-note") + " button in the title bar</p>";
914
+ empty.innerHTML = iconHtml("sticky-note") + "<p>Keep what matters across sessions</p><p class=\"notes-archive-empty-sub\">Use sticky notes for checklists, goals, handoffs, and durable project knowledge. You and every Clay agent can find them in future sessions.</p><p class=\"notes-archive-empty-sub\">Create one with the " + iconHtml("sticky-note") + " button in the title bar</p>";
962
915
  grid.appendChild(empty);
963
916
  refreshIcons();
964
917
  return;
package/lib/sdk-bridge.js CHANGED
@@ -136,7 +136,7 @@ function createSDKBridge(opts) {
136
136
  "--port", String(clayPort),
137
137
  "--slug", slug,
138
138
  "--session", String(session.localId),
139
- "--pair-only",
139
+ "--session-only",
140
140
  ];
141
141
  if (clayTls) bridgeArgs.push("--tls");
142
142
  var env = [];
@@ -553,6 +553,30 @@ function createSDKBridge(opts) {
553
553
  return { behavior: "allow", updatedInput: input };
554
554
  }
555
555
 
556
+ // Auto-approve split-pair partner tools. The pair itself is
557
+ // user-constructed (the user created the split and its roles and
558
+ // watches both panes live), the target is structurally confined to the
559
+ // split partner, and a prompt on every delegation hop defeats the
560
+ // driver/worker workflow. spawn_sessions is NOT here: it creates new
561
+ // sessions and keeps its prompt.
562
+ var pairPartnerTools = { send_to_partner: true, read_partner: true };
563
+ if (toolName.indexOf("mcp__clay-sessions__") === 0) {
564
+ var sessionsToolName = toolName.substring(toolName.lastIndexOf("__") + 2);
565
+ if (pairPartnerTools[sessionsToolName]) {
566
+ return { behavior: "allow", updatedInput: input };
567
+ }
568
+ }
569
+
570
+ // Sticky-note reads and writes are immediately visible on the shared
571
+ // board. Deletion stays outside the whitelist and therefore prompts;
572
+ // its session ownership is enforced again inside the server handler.
573
+ var notesToolName = toolName.substring(toolName.lastIndexOf("__") + 2);
574
+ var isClayNotesTool = toolName.indexOf("mcp__clay-notes__") === 0
575
+ || toolName.indexOf("clay-notes__") !== -1;
576
+ if (isClayNotesTool && (notesToolName === "list_notes" || notesToolName === "write_note")) {
577
+ return { behavior: "allow", updatedInput: input };
578
+ }
579
+
556
580
  // Auto-approve read-only email MCP tools.
557
581
  // These only read data from accounts the user explicitly checked.
558
582
  // Write operations (send, reply, mark_read) still require permission.
@@ -0,0 +1,39 @@
1
+ // Sticky-note memory tools for project sessions.
2
+
3
+ var buildShape = require("./session-spawn-mcp-server").buildShape;
4
+
5
+ var MEMORY_CONTRACT = "Shared project memory that persists across sessions and is visible to the user and other Clay agents. People may use the board freely. As a Clay agent, use notes specifically for actionable or durable work memory: checklists and to-do lists, work goals, handoffs, unfinished work, durable decisions and constraints, and knowledge that should still be available after the current session ends. Every note starts with a concise plain-text title on the first line. Add a detailed body whenever another worker would otherwise need to reconstruct the context. Keep one coherent topic per note, update an existing note instead of adding a near-duplicate, and delete notes that stop being true. Do not use notes as a transcript, a routine progress log, or an announcement of your own activity.";
6
+
7
+ function getToolDefs(handlers) {
8
+ return [
9
+ {
10
+ name: "list_notes",
11
+ description: MEMORY_CONTRACT + " List the active notes before writing when you need to avoid duplicates or inspect full memory beyond the injected summary.",
12
+ inputSchema: buildShape({}),
13
+ handler: function (args) { return handlers.list(args || {}); },
14
+ },
15
+ {
16
+ name: "write_note",
17
+ description: MEMORY_CONTRACT + " Create a new note, or update an existing note by id. Long task and handoff notes are allowed, with a generous 20000-character abuse guard.",
18
+ inputSchema: buildShape({
19
+ id: { type: "string", description: "Existing note id to update. Omit to create a note." },
20
+ text: { type: "string", description: "Sticky-note text. Put the title on the first line, followed by the detailed handoff body. The board permits up to 20000 characters." },
21
+ color: { type: "string", enum: ["yellow", "blue", "green", "pink", "orange", "purple"], description: "Optional sticky-note color." },
22
+ }, ["text"]),
23
+ handler: function (args) { return handlers.write(args || {}); },
24
+ },
25
+ {
26
+ name: "remove_note",
27
+ description: MEMORY_CONTRACT + " Remove a note created by this same session when it is no longer true. Notes created by users or other sessions cannot be removed.",
28
+ inputSchema: buildShape({
29
+ id: { type: "string", description: "Id of the note to remove." },
30
+ }, ["id"]),
31
+ handler: function (args) { return handlers.remove(args || {}); },
32
+ },
33
+ ];
34
+ }
35
+
36
+ module.exports = {
37
+ MEMORY_CONTRACT: MEMORY_CONTRACT,
38
+ getToolDefs: getToolDefs,
39
+ };
package/lib/ws-schema.js CHANGED
@@ -355,6 +355,7 @@ var schema = {
355
355
  // -----------------------------------------------------------------------
356
356
  "note_create": { direction: "c2s", handler: "lib/project-user-message.js", description: "Create a new sticky note" },
357
357
  "note_created": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Sticky note was created" },
358
+ "note_written": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Agent created or updated a sticky note" },
358
359
  "note_update": { direction: "c2s", handler: "lib/project-user-message.js", description: "Update a sticky note" },
359
360
  "note_updated": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Sticky note was updated" },
360
361
  "note_delete": { direction: "c2s", handler: "lib/project-user-message.js", description: "Delete a sticky note" },
@@ -97,6 +97,18 @@ function normalizePlanStatus(status) {
97
97
  return "pending";
98
98
  }
99
99
 
100
+ function dynamicInputSchema(inputSchema) {
101
+ if (inputSchema && typeof inputSchema.type === "string") return inputSchema;
102
+ try {
103
+ var zod = require("zod");
104
+ if (zod.toJSONSchema && inputSchema) {
105
+ var schema = inputSchema.safeParse ? inputSchema : zod.object(inputSchema);
106
+ return zod.toJSONSchema(schema);
107
+ }
108
+ } catch (e) {}
109
+ return { type: "object", properties: {} };
110
+ }
111
+
100
112
  // Detect Codex "not logged in" errors. Codex surfaces auth failures several
101
113
  // ways depending on transport: a clean error event with
102
114
  // codexErrorInfo:"unauthorized", or a turn/failed / item error whose message
@@ -813,15 +825,39 @@ function createCodexQueryHandle(appServer, queryOpts) {
813
825
  });
814
826
  return;
815
827
  }
816
- Promise.resolve(callDynamicTool(dynamicParams.tool, dynamicParams.arguments || {})).then(function (result) {
817
- var content = result && Array.isArray(result.content) ? result.content : [];
818
- var contentItems = content.map(function (item) {
819
- return { type: "inputText", text: item && item.text ? item.text : JSON.stringify(item) };
828
+ function executeDynamicTool() {
829
+ return Promise.resolve(callDynamicTool(dynamicParams.tool, dynamicParams.arguments || {})).then(function (result) {
830
+ var content = result && Array.isArray(result.content) ? result.content : [];
831
+ var contentItems = content.map(function (item) {
832
+ return { type: "inputText", text: item && item.text ? item.text : JSON.stringify(item) };
833
+ });
834
+ if (contentItems.length === 0) {
835
+ contentItems.push({ type: "inputText", text: typeof result === "string" ? result : JSON.stringify(result) });
836
+ }
837
+ appServer.respond(msg.id, { contentItems: contentItems, success: !(result && result.isError) });
820
838
  });
821
- if (contentItems.length === 0) {
822
- contentItems.push({ type: "inputText", text: typeof result === "string" ? result : JSON.stringify(result) });
839
+ }
840
+ var permissionToolName = dynamicParams.tool;
841
+ if (permissionToolName === "list_notes" || permissionToolName === "write_note" || permissionToolName === "remove_note") {
842
+ permissionToolName = "mcp__clay-notes__" + permissionToolName;
843
+ } else if (permissionToolName === "send_to_partner" || permissionToolName === "read_partner") {
844
+ permissionToolName = "mcp__clay-sessions__" + permissionToolName;
845
+ }
846
+ var permission = canUseTool
847
+ ? Promise.resolve(canUseTool(permissionToolName, dynamicParams.arguments || {}, {
848
+ toolUseID: dynamicParams.callId || String(msg.id),
849
+ signal: abortController ? abortController.signal : null,
850
+ }))
851
+ : Promise.resolve({ behavior: "allow" });
852
+ permission.then(function (decision) {
853
+ if (!isApproved(decision)) {
854
+ appServer.respond(msg.id, {
855
+ contentItems: [{ type: "inputText", text: "Tool use was not approved" }],
856
+ success: false,
857
+ });
858
+ return;
823
859
  }
824
- appServer.respond(msg.id, { contentItems: contentItems, success: !(result && result.isError) });
860
+ return executeDynamicTool();
825
861
  }).catch(function (err) {
826
862
  appServer.respond(msg.id, {
827
863
  contentItems: [{ type: "inputText", text: "Error: " + (err.message || String(err)) }],
@@ -963,7 +999,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
963
999
  type: "function",
964
1000
  name: tool.name,
965
1001
  description: tool.description || tool.name,
966
- inputSchema: tool.inputSchema || { type: "object", properties: {} },
1002
+ inputSchema: dynamicInputSchema(tool.inputSchema),
967
1003
  };
968
1004
  });
969
1005
  }
@@ -6,7 +6,7 @@
6
6
  // list/call requests to Clay's project-scoped HTTP endpoint
7
7
  // (/p/{slug}/api/mcp-bridge) when a slug is provided.
8
8
  //
9
- // Usage: node mcp-bridge-server.js --port 2633 --slug my-project [--session 42] [--pair-only]
9
+ // Usage: node mcp-bridge-server.js --port 2633 --slug my-project [--session 42] [--session-only]
10
10
  //
11
11
  // Lifecycle:
12
12
  // 1. The vendor CLI spawns this process
@@ -25,7 +25,7 @@ var clayPort = 2633;
25
25
  var claySlug = "";
26
26
  var clayTls = false;
27
27
  var claySessionId = null;
28
- var pairOnly = false;
28
+ var sessionOnly = false;
29
29
 
30
30
  for (var i = 0; i < args.length; i++) {
31
31
  if (args[i] === "--port" && args[i + 1]) {
@@ -40,8 +40,8 @@ for (var i = 0; i < args.length; i++) {
40
40
  i++;
41
41
  } else if (args[i] === "--tls") {
42
42
  clayTls = true;
43
- } else if (args[i] === "--pair-only") {
44
- pairOnly = true;
43
+ } else if (args[i] === "--session-only") {
44
+ sessionOnly = true;
45
45
  }
46
46
  }
47
47
 
@@ -119,7 +119,7 @@ function postJson(urlPath, body) {
119
119
 
120
120
  // --- Fetch tools from Clay ---
121
121
  function fetchTools() {
122
- return postJson(CLAY_MCP_PATH, { action: "list_tools", sessionId: claySessionId, pairOnly: pairOnly }).then(function (resp) {
122
+ return postJson(CLAY_MCP_PATH, { action: "list_tools", sessionId: claySessionId, sessionOnly: sessionOnly }).then(function (resp) {
123
123
  if (resp.error) {
124
124
  log("Failed to fetch tools: " + resp.error);
125
125
  return [];
@@ -146,7 +146,7 @@ function callTool(serverName, toolName, args) {
146
146
  tool: toolName,
147
147
  args: args || {},
148
148
  sessionId: claySessionId,
149
- pairOnly: pairOnly,
149
+ sessionOnly: sessionOnly,
150
150
  });
151
151
  }
152
152
 
@@ -308,4 +308,4 @@ process.stdin.on("error", function () {
308
308
  process.on("SIGTERM", function () { process.exit(0); });
309
309
  process.on("SIGINT", function () { process.exit(0); });
310
310
 
311
- log("Started: port=" + clayPort + " slug=" + claySlug + " session=" + (claySessionId === null ? "none" : claySessionId) + " pairOnly=" + pairOnly);
311
+ log("Started: port=" + clayPort + " slug=" + claySlug + " session=" + (claySessionId === null ? "none" : claySessionId) + " sessionOnly=" + sessionOnly);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "3.0.0",
3
+ "version": "3.1.0-beta.2",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",
@@ -1,178 +0,0 @@
1
- // Title-bar branch switcher for a project and its worktrees.
2
-
3
- import { store } from './store.js';
4
- import { iconHtml, refreshIcons } from './icons.js';
5
- import { escapeHtml } from './utils.js';
6
- import { familyOf } from './worktree-family.js';
7
- import { switchProject, confirmRemoveProject } from './app-projects.js';
8
- import { showWorktreeModal } from './sidebar-projects.js';
9
-
10
- var chip = null;
11
- var label = null;
12
- var menu = null;
13
- var menuOpen = false;
14
- var defaultBranches = {};
15
- // slug -> true/false once /api/branches answers; undefined while unknown.
16
- // Non-git projects hide the chip (no worktrees to manage there).
17
- var gitRepoBySlug = {};
18
-
19
- function projectName(project) {
20
- return project ? (project.title || project.project || project.name || project.slug) : "";
21
- }
22
-
23
- function currentProject(projects) {
24
- var slug = store.get('currentSlug');
25
- for (var i = 0; i < projects.length; i++) {
26
- if (projects[i].slug === slug) return projects[i];
27
- }
28
- return null;
29
- }
30
-
31
- function defaultBranch(parent) {
32
- return defaultBranches[parent.slug] || "default";
33
- }
34
-
35
- function requestDefaultBranch(parent) {
36
- if (!parent || Object.prototype.hasOwnProperty.call(defaultBranches, parent.slug)) return;
37
- defaultBranches[parent.slug] = null;
38
- fetch("/p/" + encodeURIComponent(parent.slug) + "/api/branches")
39
- .then(function (response) { return response.json(); })
40
- .then(function (data) {
41
- defaultBranches[parent.slug] = data.defaultBranch || "default";
42
- gitRepoBySlug[parent.slug] = data.isGitRepo !== false;
43
- renderBranchSwitcher();
44
- })
45
- .catch(function () {
46
- defaultBranches[parent.slug] = "default";
47
- renderBranchSwitcher();
48
- });
49
- }
50
-
51
- function closeMenu() {
52
- menuOpen = false;
53
- if (menu) menu.classList.add("hidden");
54
- if (chip) {
55
- chip.classList.remove("open");
56
- chip.setAttribute("aria-expanded", "false");
57
- }
58
- }
59
-
60
- function positionMenu() {
61
- if (!chip || !menu || !menuOpen) return;
62
- var rect = chip.getBoundingClientRect();
63
- var left = Math.min(rect.left, window.innerWidth - menu.offsetWidth - 8);
64
- var top = rect.bottom + 6;
65
- if (top + menu.offsetHeight > window.innerHeight - 8) top = rect.top - menu.offsetHeight - 6;
66
- menu.style.left = Math.max(8, left) + "px";
67
- menu.style.top = Math.max(8, top) + "px";
68
- }
69
-
70
- function branchLabel(project, parent) {
71
- return project.isWorktree ? (project.branch || projectName(project)) : defaultBranch(parent);
72
- }
73
-
74
- function appendProjectRow(container, project, parent, isCurrent) {
75
- var row = document.createElement("div");
76
- row.className = "branch-chip-row" + (isCurrent ? " current" : "");
77
- var inaccessible = project.isWorktree && project.worktreeAccessible === false;
78
- var main = document.createElement("button");
79
- main.className = "branch-chip-row-main";
80
- main.disabled = inaccessible;
81
- if (inaccessible) main.title = "Outside project path";
82
- main.innerHTML = iconHtml("git-branch") + '<span class="branch-chip-row-label">' +
83
- escapeHtml(branchLabel(project, parent)) + "</span>";
84
- if (project.isProcessing) {
85
- var dot = document.createElement("span");
86
- dot.className = "branch-chip-processing";
87
- main.appendChild(dot);
88
- }
89
- if (project.pendingPermissions > 0) {
90
- var badge = document.createElement("span");
91
- badge.className = "branch-chip-pending";
92
- badge.textContent = project.pendingPermissions > 99 ? "99+" : String(project.pendingPermissions);
93
- main.appendChild(badge);
94
- }
95
- if (isCurrent) main.insertAdjacentHTML("beforeend", iconHtml("check"));
96
- if (!inaccessible) {
97
- main.addEventListener("click", function () { closeMenu(); switchProject(project.slug); });
98
- }
99
- row.appendChild(main);
100
-
101
- if (project.isWorktree && (!store.get('permissions') || store.get('permissions').deleteProject !== false)) {
102
- var remove = document.createElement("button");
103
- remove.className = "branch-chip-row-action";
104
- remove.title = "Remove worktree";
105
- remove.innerHTML = iconHtml("trash-2");
106
- remove.addEventListener("click", function (event) {
107
- event.stopPropagation();
108
- closeMenu();
109
- confirmRemoveProject(project.slug, projectName(project));
110
- });
111
- row.appendChild(remove);
112
- }
113
- container.appendChild(row);
114
- }
115
-
116
- function renderMenu(family, current) {
117
- menu.innerHTML = "";
118
- if (family.parent) appendProjectRow(menu, family.parent, family.parent, current.slug === family.parent.slug);
119
- for (var i = 0; i < family.worktrees.length; i++) {
120
- appendProjectRow(menu, family.worktrees[i], family.parent || family.worktrees[i], current.slug === family.worktrees[i].slug);
121
- }
122
- if (family.parent) {
123
- var footer = document.createElement("button");
124
- footer.className = "branch-chip-new";
125
- footer.innerHTML = iconHtml("plus") + " <span>New worktree...</span>";
126
- footer.addEventListener("click", function () {
127
- closeMenu();
128
- showWorktreeModal(family.parent.slug, projectName(family.parent));
129
- });
130
- menu.appendChild(footer);
131
- }
132
- refreshIcons();
133
- positionMenu();
134
- }
135
-
136
- export function renderBranchSwitcher() {
137
- if (!chip) return;
138
- var projects = store.get('projects') || [];
139
- var current = currentProject(projects);
140
- var family = current ? familyOf(projects, current.slug) : { parent: null, worktrees: [] };
141
- // The chip is the project's worktree management surface, so it shows even
142
- // with zero worktrees (the menu then offers the default branch and "New
143
- // worktree"). Hidden where there is no project context to manage, and for
144
- // non-git projects (optimistically visible until /api/branches answers).
145
- var gateSlug = family.parent ? family.parent.slug : (current ? current.slug : null);
146
- var hidden = !current || store.get('dmMode') || store.get('mateProjectSlug') ||
147
- store.get('homeHubVisible') || (gateSlug && gitRepoBySlug[gateSlug] === false);
148
- chip.classList.toggle("hidden", hidden);
149
- if (hidden) { closeMenu(); return; }
150
- if (family.parent) requestDefaultBranch(family.parent);
151
- label.textContent = branchLabel(current, family.parent || current);
152
- if (menuOpen) renderMenu(family, current);
153
- }
154
-
155
- export function initBranchSwitcher() {
156
- chip = document.getElementById("branch-chip");
157
- label = document.getElementById("branch-chip-label");
158
- menu = document.getElementById("branch-chip-menu");
159
- if (!chip || !label || !menu) return;
160
- chip.addEventListener("click", function (event) {
161
- event.stopPropagation();
162
- menuOpen = !menuOpen;
163
- chip.classList.toggle("open", menuOpen);
164
- chip.setAttribute("aria-expanded", menuOpen ? "true" : "false");
165
- menu.classList.toggle("hidden", !menuOpen);
166
- renderBranchSwitcher();
167
- });
168
- document.addEventListener("click", function (event) {
169
- if (menuOpen && !menu.contains(event.target) && !chip.contains(event.target)) closeMenu();
170
- });
171
- window.addEventListener("resize", positionMenu);
172
- store.subscribe(function (state, prev) {
173
- if (state.projects !== prev.projects || state.currentSlug !== prev.currentSlug ||
174
- state.dmMode !== prev.dmMode || state.mateProjectSlug !== prev.mateProjectSlug ||
175
- state.homeHubVisible !== prev.homeHubVisible) renderBranchSwitcher();
176
- });
177
- renderBranchSwitcher();
178
- }