clay-server 3.0.0 → 3.1.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.
- package/lib/notes.js +6 -0
- package/lib/project-http.js +3 -6
- package/lib/project-session-notes.js +217 -0
- package/lib/project.js +84 -18
- package/lib/public/app.js +1 -4
- package/lib/public/css/icon-strip.css +190 -0
- package/lib/public/css/messages.css +104 -2
- package/lib/public/css/mobile-nav.css +0 -4
- package/lib/public/css/pane.css +27 -0
- package/lib/public/css/rewind.css +10 -0
- package/lib/public/css/sticky-notes.css +18 -0
- package/lib/public/css/title-bar.css +0 -169
- package/lib/public/index.html +0 -6
- package/lib/public/modules/app-favicon.js +10 -1
- package/lib/public/modules/app-home-hub.js +0 -2
- package/lib/public/modules/app-messages.js +18 -6
- package/lib/public/modules/app-projects.js +30 -43
- package/lib/public/modules/app-rendering.js +42 -3
- package/lib/public/modules/dom-refs.js +1 -0
- package/lib/public/modules/input.js +13 -0
- package/lib/public/modules/project-switcher.js +1 -2
- package/lib/public/modules/sidebar-mobile.js +6 -19
- package/lib/public/modules/sidebar-projects.js +232 -63
- package/lib/public/modules/split-pair-ui.js +30 -0
- package/lib/public/modules/sticky-notes.js +21 -0
- package/lib/sdk-bridge.js +25 -1
- package/lib/session-notes-mcp-server.js +39 -0
- package/lib/ws-schema.js +1 -0
- package/lib/yoke/adapters/codex.js +44 -8
- package/lib/yoke/mcp-bridge-server.js +7 -7
- package/package.json +1 -1
- package/lib/public/modules/branch-switcher.js +0 -178
- package/lib/public/modules/worktree-family.js +0 -60
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
|
-
"--
|
|
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 on the user's board. One note = one fact, decision, or reminder, written like a real sticky note: a phrase or at most two short sentences. It must be worth a human's attention a week later and stand on its own without the current conversation. Never paragraphs, lists of steps, logs, or announcement notes about your own activity. Update an existing note instead of adding a near-duplicate; delete notes that stop being true.";
|
|
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 board notes are allowed, with a 2000-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. Keep the register concise even though the board permits up to 2000 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
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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
|
-
|
|
822
|
-
|
|
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
|
-
|
|
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
|
|
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] [--
|
|
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
|
|
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] === "--
|
|
44
|
-
|
|
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,
|
|
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
|
-
|
|
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) + "
|
|
311
|
+
log("Started: port=" + clayPort + " slug=" + claySlug + " session=" + (claySessionId === null ? "none" : claySessionId) + " sessionOnly=" + sessionOnly);
|
package/package.json
CHANGED
|
@@ -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
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
// Pure helpers for presenting worktree projects as branches of one family.
|
|
2
|
-
|
|
3
|
-
function projectName(project) {
|
|
4
|
-
return project ? (project.title || project.project || project.name || project.slug) : "";
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function familyOf(projects, slug) {
|
|
8
|
-
var list = projects || [];
|
|
9
|
-
var current = null;
|
|
10
|
-
for (var i = 0; i < list.length; i++) {
|
|
11
|
-
if (list[i].slug === slug) { current = list[i]; break; }
|
|
12
|
-
}
|
|
13
|
-
if (!current) return { parent: null, worktrees: [] };
|
|
14
|
-
|
|
15
|
-
var parentSlug = current.isWorktree ? current.parentSlug : current.slug;
|
|
16
|
-
var parent = current.isWorktree ? null : current;
|
|
17
|
-
var worktrees = [];
|
|
18
|
-
for (var j = 0; j < list.length; j++) {
|
|
19
|
-
var project = list[j];
|
|
20
|
-
if (!parent && project.slug === parentSlug && !project.isWorktree) parent = project;
|
|
21
|
-
if (project.isWorktree && project.parentSlug === parentSlug) worktrees.push(project);
|
|
22
|
-
}
|
|
23
|
-
if (current.isWorktree && worktrees.indexOf(current) === -1) worktrees.push(current);
|
|
24
|
-
worktrees.sort(function (a, b) { return projectName(a).localeCompare(projectName(b)); });
|
|
25
|
-
return { parent: parent, worktrees: worktrees };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function parentProjects(projects) {
|
|
29
|
-
return (projects || []).filter(function (project) { return !project.isWorktree; });
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function displayProject(projects, slug) {
|
|
33
|
-
var family = familyOf(projects, slug);
|
|
34
|
-
if (family.parent) return family.parent;
|
|
35
|
-
for (var i = 0; i < (projects || []).length; i++) {
|
|
36
|
-
if (projects[i].slug === slug) return projects[i];
|
|
37
|
-
}
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function aggregateFamily(parent, worktrees) {
|
|
42
|
-
var result = Object.assign({}, parent);
|
|
43
|
-
var children = worktrees || [];
|
|
44
|
-
result.isProcessing = !!result.isProcessing;
|
|
45
|
-
result.unread = result.unread || 0;
|
|
46
|
-
result.pendingPermissions = result.pendingPermissions || 0;
|
|
47
|
-
for (var i = 0; i < children.length; i++) {
|
|
48
|
-
result.isProcessing = result.isProcessing || !!children[i].isProcessing;
|
|
49
|
-
result.unread += children[i].unread || 0;
|
|
50
|
-
result.pendingPermissions += children[i].pendingPermissions || 0;
|
|
51
|
-
}
|
|
52
|
-
return result;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function switcherProjectName(projects, project) {
|
|
56
|
-
if (!project || !project.isWorktree) return projectName(project);
|
|
57
|
-
var family = familyOf(projects, project.slug);
|
|
58
|
-
var parentName = family.parent ? projectName(family.parent) : (project.parentSlug || "Project");
|
|
59
|
-
return parentName + " \u2387 " + (project.branch || projectName(project));
|
|
60
|
-
}
|