clay-server 4.0.0-beta.13 → 4.0.0-beta.15
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/capsule-display-floor.js +51 -0
- package/lib/capsule-frame-server.js +229 -0
- package/lib/capsule-pig-logic.js +268 -0
- package/lib/capsule-server-runtimes.js +41 -0
- package/lib/capsule-tictactoe-logic.js +260 -0
- package/lib/capsules/pig/display.js +190 -0
- package/lib/capsules/pig/manifest.json +9 -0
- package/lib/capsules/pig/ui.json +46 -0
- package/lib/capsules/tictactoe/manifest.json +9 -0
- package/lib/capsules/tictactoe/ui.json +203 -0
- package/lib/project-capsule-catalog.js +9 -1
- package/lib/project-connection.js +1 -1
- package/lib/project-log-feedback-delivery.js +76 -0
- package/lib/project-logs.js +4 -0
- package/lib/project-pair-lifecycle.js +7 -12
- package/lib/project-session-pair.js +29 -39
- package/lib/project-worker-proposal.js +203 -72
- package/lib/project.js +13 -0
- package/lib/public/css/capsule-ui.css +18 -0
- package/lib/public/css/home-session-actions.css +66 -0
- package/lib/public/css/home-sidebar.css +66 -0
- package/lib/public/css/mobile-nav.css +80 -0
- package/lib/public/css/sidebar.css +92 -0
- package/lib/public/css/worker-proposal.css +34 -1
- package/lib/public/modules/app-messages.js +14 -1
- package/lib/public/modules/home-conversations-sheet.js +102 -48
- package/lib/public/modules/home-session-actions.js +6 -0
- package/lib/public/modules/home-sidebar-chat-list.js +101 -41
- package/lib/public/modules/home-tool-frame.js +165 -0
- package/lib/public/modules/home-tools.js +129 -1
- package/lib/public/modules/session-hierarchy.js +54 -0
- package/lib/public/modules/sidebar-mobile.js +35 -6
- package/lib/public/modules/sidebar-session-hierarchy.js +206 -0
- package/lib/public/modules/sidebar-sessions.js +49 -7
- package/lib/public/modules/worker-proposal-state.js +16 -0
- package/lib/public/modules/worker-proposal.js +49 -12
- package/lib/sdk-bridge.js +16 -8
- package/lib/sdk-message-processor.js +34 -4
- package/lib/server-home-chat.js +12 -1
- package/lib/server-tools.js +97 -9
- package/lib/server.js +3 -0
- package/lib/session-driver-eligibility.js +20 -165
- package/lib/session-pair-factory.js +31 -28
- package/lib/session-pair-mcp-server.js +5 -9
- package/lib/session-pair-prompts.js +15 -15
- package/lib/session-provenance.js +119 -0
- package/lib/session-spawn-mcp-server.js +1 -1
- package/lib/sessions.js +40 -2
- package/lib/tools-registry.js +44 -10
- package/lib/ws-schema.js +8 -3
- package/lib/yoke/adapters/claude.js +12 -0
- package/package.json +1 -1
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// Shared Driver/Worker hierarchy behavior for the project desktop and mobile lists.
|
|
2
|
+
|
|
3
|
+
import { iconHtml } from './icons.js';
|
|
4
|
+
import { store } from './store.js';
|
|
5
|
+
import { buildSessionHierarchy } from './session-hierarchy.js';
|
|
6
|
+
|
|
7
|
+
var expansionBySurface = {
|
|
8
|
+
desktop: new Map(),
|
|
9
|
+
mobile: new Map(),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function prepareSidebarHierarchy(sessions) {
|
|
13
|
+
var hierarchy = buildSessionHierarchy(sessions);
|
|
14
|
+
var byDriver = new Map();
|
|
15
|
+
var sessionIds = new Set();
|
|
16
|
+
for (var i = 0; i < hierarchy.roots.length; i++) {
|
|
17
|
+
var root = hierarchy.roots[i];
|
|
18
|
+
if (!root.workers.length) continue;
|
|
19
|
+
byDriver.set(root.driver.id, root);
|
|
20
|
+
sessionIds.add(root.driver.id);
|
|
21
|
+
for (var j = 0; j < root.workers.length; j++) sessionIds.add(root.workers[j].id);
|
|
22
|
+
}
|
|
23
|
+
for (var k = 0; k < hierarchy.orphans.length; k++) sessionIds.add(hierarchy.orphans[k].id);
|
|
24
|
+
return {
|
|
25
|
+
byDriver: byDriver,
|
|
26
|
+
orphans: hierarchy.orphans,
|
|
27
|
+
sessionIds: sessionIds,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function currentWorkerIds() {
|
|
32
|
+
var result = new Set();
|
|
33
|
+
var groups = store.get('splitGroups') || [];
|
|
34
|
+
for (var i = 0; i < groups.length; i++) {
|
|
35
|
+
if (groups[i].pair && Number.isInteger(groups[i].pair.workerId)) result.add(groups[i].pair.workerId);
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function hierarchyItemMatches(item, matchIds) {
|
|
41
|
+
if (matchIds === null) return true;
|
|
42
|
+
if (item.type === "driver-hierarchy") {
|
|
43
|
+
if (matchIds.has(item.root.driver.id)) return true;
|
|
44
|
+
for (var i = 0; i < item.root.workers.length; i++) {
|
|
45
|
+
if (matchIds.has(item.root.workers[i].id)) return true;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (item.type === "orphan-workers") {
|
|
50
|
+
for (var j = 0; j < item.workers.length; j++) {
|
|
51
|
+
if (matchIds.has(item.workers[j].id)) return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function expanded(surface, key, workers, matchIds) {
|
|
59
|
+
var state = expansionBySurface[surface];
|
|
60
|
+
if (state.has(key)) return state.get(key);
|
|
61
|
+
return defaultHierarchyExpanded(workers, matchIds, currentWorkerIds());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function defaultHierarchyExpanded(workers, matchIds, current) {
|
|
65
|
+
var currentIds = current || new Set();
|
|
66
|
+
for (var i = 0; i < workers.length; i++) {
|
|
67
|
+
if (workers[i].active || currentIds.has(workers[i].id) || (matchIds !== null && matchIds.has(workers[i].id))) return true;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function toggle(surface, key, wasExpanded, rerender) {
|
|
73
|
+
expansionBySurface[surface].set(key, !wasExpanded);
|
|
74
|
+
rerender();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function visibleWorker(worker, driver, matchIds) {
|
|
78
|
+
return matchIds === null || matchIds.has(worker.id) || matchIds.has(driver.id);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function renderDesktopDriverHierarchy(root, renderSession, rerender, matchIds) {
|
|
82
|
+
var wrapper = document.createElement("div");
|
|
83
|
+
wrapper.className = "session-driver-hierarchy";
|
|
84
|
+
var key = "driver:" + root.driver.id;
|
|
85
|
+
var isExpanded = expanded("desktop", key, root.workers, matchIds);
|
|
86
|
+
var childrenId = "session-workers-" + root.driver.id;
|
|
87
|
+
var row = renderSession(root.driver);
|
|
88
|
+
row.classList.add("session-driver-item");
|
|
89
|
+
var control = document.createElement("button");
|
|
90
|
+
control.type = "button";
|
|
91
|
+
control.className = "session-driver-toggle";
|
|
92
|
+
control.setAttribute("aria-expanded", String(isExpanded));
|
|
93
|
+
control.setAttribute("aria-controls", childrenId);
|
|
94
|
+
control.setAttribute("aria-label", (isExpanded ? "Collapse" : "Expand") + " Workers for " + (root.driver.title || "New Session"));
|
|
95
|
+
control.innerHTML = iconHtml("chevron-right");
|
|
96
|
+
control.addEventListener("click", function (event) {
|
|
97
|
+
event.preventDefault();
|
|
98
|
+
event.stopPropagation();
|
|
99
|
+
toggle("desktop", key, isExpanded, rerender);
|
|
100
|
+
});
|
|
101
|
+
row.insertBefore(control, row.firstChild);
|
|
102
|
+
wrapper.appendChild(row);
|
|
103
|
+
|
|
104
|
+
var children = document.createElement("div");
|
|
105
|
+
children.id = childrenId;
|
|
106
|
+
children.className = "session-worker-children";
|
|
107
|
+
children.setAttribute("role", "group");
|
|
108
|
+
children.setAttribute("aria-label", "Workers for " + (root.driver.title || "New Session"));
|
|
109
|
+
children.hidden = !isExpanded;
|
|
110
|
+
var current = currentWorkerIds();
|
|
111
|
+
for (var i = 0; i < root.workers.length; i++) {
|
|
112
|
+
if (!visibleWorker(root.workers[i], root.driver, matchIds)) continue;
|
|
113
|
+
children.appendChild(renderSession(root.workers[i], { worker: true, current: current.has(root.workers[i].id) }));
|
|
114
|
+
}
|
|
115
|
+
wrapper.appendChild(children);
|
|
116
|
+
return wrapper;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function renderDesktopOrphanHierarchy(workers, renderSession, rerender, matchIds) {
|
|
120
|
+
var wrapper = document.createElement("div");
|
|
121
|
+
wrapper.className = "session-driver-hierarchy session-orphan-hierarchy";
|
|
122
|
+
var isExpanded = expanded("desktop", "orphan", workers, matchIds);
|
|
123
|
+
var control = document.createElement("button");
|
|
124
|
+
control.type = "button";
|
|
125
|
+
control.className = "session-orphan-toggle";
|
|
126
|
+
control.setAttribute("aria-expanded", String(isExpanded));
|
|
127
|
+
control.setAttribute("aria-controls", "session-orphan-workers");
|
|
128
|
+
control.innerHTML = iconHtml("chevron-right") + '<span class="session-orphan-title">Unavailable Driver</span><span class="session-worker-count">' + workers.length + "</span>";
|
|
129
|
+
control.addEventListener("click", function () { toggle("desktop", "orphan", isExpanded, rerender); });
|
|
130
|
+
wrapper.appendChild(control);
|
|
131
|
+
|
|
132
|
+
var children = document.createElement("div");
|
|
133
|
+
children.id = "session-orphan-workers";
|
|
134
|
+
children.className = "session-worker-children";
|
|
135
|
+
children.setAttribute("role", "group");
|
|
136
|
+
children.setAttribute("aria-label", "Workers whose Driver is unavailable");
|
|
137
|
+
children.hidden = !isExpanded;
|
|
138
|
+
var current = currentWorkerIds();
|
|
139
|
+
for (var i = 0; i < workers.length; i++) {
|
|
140
|
+
if (matchIds !== null && !matchIds.has(workers[i].id)) continue;
|
|
141
|
+
children.appendChild(renderSession(workers[i], { worker: true, current: current.has(workers[i].id) }));
|
|
142
|
+
}
|
|
143
|
+
wrapper.appendChild(children);
|
|
144
|
+
return wrapper;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function renderMobileDriverHierarchy(root, renderSession, rerender) {
|
|
148
|
+
var wrapper = document.createElement("div");
|
|
149
|
+
wrapper.className = "mobile-driver-hierarchy";
|
|
150
|
+
var key = "driver:" + root.driver.id;
|
|
151
|
+
var isExpanded = expanded("mobile", key, root.workers, null);
|
|
152
|
+
var childrenId = "mobile-session-workers-" + root.driver.id;
|
|
153
|
+
var header = document.createElement("div");
|
|
154
|
+
header.className = "mobile-driver-header";
|
|
155
|
+
var control = document.createElement("button");
|
|
156
|
+
control.type = "button";
|
|
157
|
+
control.className = "mobile-driver-toggle";
|
|
158
|
+
control.setAttribute("aria-expanded", String(isExpanded));
|
|
159
|
+
control.setAttribute("aria-controls", childrenId);
|
|
160
|
+
control.setAttribute("aria-label", (isExpanded ? "Collapse" : "Expand") + " Workers for " + (root.driver.title || "New Session"));
|
|
161
|
+
control.innerHTML = iconHtml("chevron-right");
|
|
162
|
+
control.addEventListener("click", function () { toggle("mobile", key, isExpanded, rerender); });
|
|
163
|
+
header.appendChild(control);
|
|
164
|
+
header.appendChild(renderSession(root.driver));
|
|
165
|
+
wrapper.appendChild(header);
|
|
166
|
+
|
|
167
|
+
var children = document.createElement("div");
|
|
168
|
+
children.id = childrenId;
|
|
169
|
+
children.className = "mobile-worker-children";
|
|
170
|
+
children.setAttribute("role", "group");
|
|
171
|
+
children.setAttribute("aria-label", "Workers for " + (root.driver.title || "New Session"));
|
|
172
|
+
children.hidden = !isExpanded;
|
|
173
|
+
var current = currentWorkerIds();
|
|
174
|
+
for (var i = 0; i < root.workers.length; i++) {
|
|
175
|
+
children.appendChild(renderSession(root.workers[i], { worker: true, current: current.has(root.workers[i].id) }));
|
|
176
|
+
}
|
|
177
|
+
wrapper.appendChild(children);
|
|
178
|
+
return wrapper;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function renderMobileOrphanHierarchy(workers, renderSession, rerender) {
|
|
182
|
+
var wrapper = document.createElement("div");
|
|
183
|
+
wrapper.className = "mobile-driver-hierarchy mobile-orphan-hierarchy";
|
|
184
|
+
var isExpanded = expanded("mobile", "orphan", workers, null);
|
|
185
|
+
var control = document.createElement("button");
|
|
186
|
+
control.type = "button";
|
|
187
|
+
control.className = "mobile-orphan-toggle";
|
|
188
|
+
control.setAttribute("aria-expanded", String(isExpanded));
|
|
189
|
+
control.setAttribute("aria-controls", "mobile-orphan-workers");
|
|
190
|
+
control.innerHTML = iconHtml("chevron-right") + "<span>Unavailable Driver</span><span>" + workers.length + "</span>";
|
|
191
|
+
control.addEventListener("click", function () { toggle("mobile", "orphan", isExpanded, rerender); });
|
|
192
|
+
wrapper.appendChild(control);
|
|
193
|
+
|
|
194
|
+
var children = document.createElement("div");
|
|
195
|
+
children.id = "mobile-orphan-workers";
|
|
196
|
+
children.className = "mobile-worker-children";
|
|
197
|
+
children.setAttribute("role", "group");
|
|
198
|
+
children.setAttribute("aria-label", "Workers whose Driver is unavailable");
|
|
199
|
+
children.hidden = !isExpanded;
|
|
200
|
+
var current = currentWorkerIds();
|
|
201
|
+
for (var i = 0; i < workers.length; i++) {
|
|
202
|
+
children.appendChild(renderSession(workers[i], { worker: true, current: current.has(workers[i].id) }));
|
|
203
|
+
}
|
|
204
|
+
wrapper.appendChild(children);
|
|
205
|
+
return wrapper;
|
|
206
|
+
}
|
|
@@ -16,6 +16,12 @@ import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER, VENDOR_HOMEPAGES, isExperim
|
|
|
16
16
|
import { openGroup, separateGroup } from './split-view.js';
|
|
17
17
|
import { groupedSessionIds } from './split-group-helpers.js';
|
|
18
18
|
import { openPairDialog } from './split-pair-ui.js';
|
|
19
|
+
import {
|
|
20
|
+
hierarchyItemMatches,
|
|
21
|
+
prepareSidebarHierarchy,
|
|
22
|
+
renderDesktopDriverHierarchy,
|
|
23
|
+
renderDesktopOrphanHierarchy
|
|
24
|
+
} from './sidebar-session-hierarchy.js';
|
|
19
25
|
|
|
20
26
|
|
|
21
27
|
// --- Session state ---
|
|
@@ -49,8 +55,8 @@ function sendSessionBookmark(sessionId, bookmarked) {
|
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
function compareSessionListItems(a, b) {
|
|
52
|
-
var aData = a && a.type === "session" ? a.data : a;
|
|
53
|
-
var bData = b && b.type === "session" ? b.data : b;
|
|
58
|
+
var aData = a && (a.type === "session" || a.type === "driver-hierarchy") ? a.data : a;
|
|
59
|
+
var bData = b && (b.type === "session" || b.type === "driver-hierarchy") ? b.data : b;
|
|
54
60
|
var aBookmarked = !!(aData && aData.bookmarked);
|
|
55
61
|
var bBookmarked = !!(bData && bData.bookmarked);
|
|
56
62
|
if (aBookmarked !== bBookmarked) return aBookmarked ? -1 : 1;
|
|
@@ -236,6 +242,14 @@ function collectItemSessionIds(item) {
|
|
|
236
242
|
if (item.type === "split-group" && Array.isArray(item.members)) {
|
|
237
243
|
return item.members.map(function (session) { return session.id; });
|
|
238
244
|
}
|
|
245
|
+
if (item.type === "driver-hierarchy" && item.root) {
|
|
246
|
+
var hierarchyIds = [item.root.driver.id];
|
|
247
|
+
for (var j = 0; j < item.root.workers.length; j++) hierarchyIds.push(item.root.workers[j].id);
|
|
248
|
+
return hierarchyIds;
|
|
249
|
+
}
|
|
250
|
+
if (item.type === "orphan-workers" && Array.isArray(item.workers)) {
|
|
251
|
+
return item.workers.map(function (session) { return session.id; });
|
|
252
|
+
}
|
|
239
253
|
return [];
|
|
240
254
|
}
|
|
241
255
|
|
|
@@ -1246,10 +1260,12 @@ function renderLoopRun(parentGk, startedAtKey, sessions, isRalph) {
|
|
|
1246
1260
|
|
|
1247
1261
|
// --- Session item rendering ---
|
|
1248
1262
|
|
|
1249
|
-
function renderSessionItem(s) {
|
|
1263
|
+
function renderSessionItem(s, options) {
|
|
1264
|
+
var itemOptions = options || {};
|
|
1250
1265
|
var el = document.createElement("div");
|
|
1251
1266
|
var isMatch = searchMatchIds !== null && searchMatchIds.has(s.id);
|
|
1252
1267
|
el.className = "session-item" + (s.active ? " active" : "") + (isMatch ? " search-match" : "");
|
|
1268
|
+
if (itemOptions.worker) el.classList.add("session-worker-item");
|
|
1253
1269
|
el.dataset.sessionId = s.id;
|
|
1254
1270
|
|
|
1255
1271
|
// Keep agent identity separate from the title so each row has a stable
|
|
@@ -1281,6 +1297,13 @@ function renderSessionItem(s) {
|
|
|
1281
1297
|
textSpan.innerHTML = textHtml;
|
|
1282
1298
|
el.appendChild(textSpan);
|
|
1283
1299
|
|
|
1300
|
+
if (itemOptions.worker) {
|
|
1301
|
+
var workerLabel = document.createElement("span");
|
|
1302
|
+
workerLabel.className = "session-worker-generation" + (itemOptions.current ? " current" : "");
|
|
1303
|
+
workerLabel.textContent = itemOptions.current ? "Current" : (s.workerGeneration ? "Gen " + s.workerGeneration : "Worker");
|
|
1304
|
+
el.appendChild(workerLabel);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1284
1307
|
var ageText = relativeTime(s.lastActivity || 0);
|
|
1285
1308
|
if (ageText) {
|
|
1286
1309
|
var age = document.createElement("span");
|
|
@@ -1325,7 +1348,7 @@ function renderSessionItem(s) {
|
|
|
1325
1348
|
|
|
1326
1349
|
// Presence avatars (multi-user)
|
|
1327
1350
|
renderPresenceAvatars(el, String(s.id));
|
|
1328
|
-
setupSessionDragHandlers(el, s);
|
|
1351
|
+
if (!itemOptions.worker) setupSessionDragHandlers(el, s);
|
|
1329
1352
|
|
|
1330
1353
|
return el;
|
|
1331
1354
|
}
|
|
@@ -1409,12 +1432,17 @@ export function renderSessionList(sessions) {
|
|
|
1409
1432
|
// Group by loopId + date so all runs of the same task on the same day are merged
|
|
1410
1433
|
var splitGroups = store.get('splitGroups') || [];
|
|
1411
1434
|
var groupedIds = groupedSessionIds(splitGroups);
|
|
1435
|
+
var hierarchy = prepareSidebarHierarchy(cachedSessions);
|
|
1436
|
+
var hierarchyByDriver = hierarchy.byDriver;
|
|
1437
|
+
var hierarchySessionIds = hierarchy.sessionIds;
|
|
1438
|
+
hierarchySessionIds.forEach(function (id) { groupedIds.delete(id); });
|
|
1412
1439
|
var sessionsById = new Map();
|
|
1413
1440
|
for (var si = 0; si < cachedSessions.length; si++) sessionsById.set(cachedSessions[si].id, cachedSessions[si]);
|
|
1414
1441
|
var loopGroups = {}; // groupKey -> [sessions]
|
|
1415
1442
|
var normalSessions = [];
|
|
1416
1443
|
for (var i = 0; i < cachedSessions.length; i++) {
|
|
1417
1444
|
var s = cachedSessions[i];
|
|
1445
|
+
if (s.sessionRole === "worker") continue;
|
|
1418
1446
|
if (groupedIds.has(s.id)) continue;
|
|
1419
1447
|
if (s.loop && s.loop.loopId && s.loop.role === "crafting" && s.loop.source !== "ralph" && s.loop.source !== "debate") {
|
|
1420
1448
|
// Task crafting sessions live in the scheduler calendar, not the main list (except debate)
|
|
@@ -1433,13 +1461,21 @@ export function renderSessionList(sessions) {
|
|
|
1433
1461
|
// Build virtual items: normal sessions + one entry per loop group (using latest child's lastActivity)
|
|
1434
1462
|
var items = [];
|
|
1435
1463
|
for (var j = 0; j < normalSessions.length; j++) {
|
|
1436
|
-
|
|
1464
|
+
var driverRoot = hierarchyByDriver.get(normalSessions[j].id);
|
|
1465
|
+
if (driverRoot) items.push({ type: "driver-hierarchy", root: driverRoot, data: driverRoot.driver, lastActivity: driverRoot.lastActivity });
|
|
1466
|
+
else items.push({ type: "session", data: normalSessions[j], lastActivity: normalSessions[j].lastActivity || 0 });
|
|
1467
|
+
}
|
|
1468
|
+
if (hierarchy.orphans.length) {
|
|
1469
|
+
var orphanActivity = 0;
|
|
1470
|
+
for (var oi = 0; oi < hierarchy.orphans.length; oi++) orphanActivity = Math.max(orphanActivity, hierarchy.orphans[oi].lastActivity || 0);
|
|
1471
|
+
items.push({ type: "orphan-workers", workers: hierarchy.orphans, lastActivity: orphanActivity });
|
|
1437
1472
|
}
|
|
1438
1473
|
for (var sg = 0; sg < splitGroups.length; sg++) {
|
|
1439
1474
|
var splitGroup = splitGroups[sg];
|
|
1440
1475
|
var leftMember = sessionsById.get(splitGroup.members[0]);
|
|
1441
1476
|
var rightMember = sessionsById.get(splitGroup.members[1]);
|
|
1442
1477
|
if (!leftMember || !rightMember) continue;
|
|
1478
|
+
if (hierarchySessionIds.has(leftMember.id) || hierarchySessionIds.has(rightMember.id)) continue;
|
|
1443
1479
|
items.push({
|
|
1444
1480
|
type: "split-group",
|
|
1445
1481
|
group: splitGroup,
|
|
@@ -1470,9 +1506,10 @@ export function renderSessionList(sessions) {
|
|
|
1470
1506
|
if (item.type === "session" && item.data && !isSessionVisibleBySearch(item.data.id)) {
|
|
1471
1507
|
continue;
|
|
1472
1508
|
}
|
|
1509
|
+
if ((item.type === "driver-hierarchy" || item.type === "orphan-workers") && !hierarchyItemMatches(item, searchMatchIds)) continue;
|
|
1473
1510
|
if (item.type === "split-group" && searchMatchIds !== null &&
|
|
1474
1511
|
!searchMatchIds.has(item.members[0].id) && !searchMatchIds.has(item.members[1].id)) continue;
|
|
1475
|
-
if (item.type === "session" && item.data && item.data.bookmarked) {
|
|
1512
|
+
if ((item.type === "session" || item.type === "driver-hierarchy") && item.data && item.data.bookmarked) {
|
|
1476
1513
|
bookmarkedItems.push(item);
|
|
1477
1514
|
} else {
|
|
1478
1515
|
regularItems.push(item);
|
|
@@ -1489,7 +1526,8 @@ export function renderSessionList(sessions) {
|
|
|
1489
1526
|
favoritesContainer.appendChild(emptyHint);
|
|
1490
1527
|
}
|
|
1491
1528
|
for (var bi = 0; bi < bookmarkedItems.length; bi++) {
|
|
1492
|
-
favoritesContainer.appendChild(
|
|
1529
|
+
if (bookmarkedItems[bi].type === "driver-hierarchy") favoritesContainer.appendChild(renderDesktopDriverHierarchy(bookmarkedItems[bi].root, renderSessionItem, function () { renderSessionList(null); }, searchMatchIds));
|
|
1530
|
+
else favoritesContainer.appendChild(renderSessionItem(bookmarkedItems[bi].data));
|
|
1493
1531
|
}
|
|
1494
1532
|
|
|
1495
1533
|
var divider = document.createElement("div");
|
|
@@ -1529,6 +1567,10 @@ export function renderSessionList(sessions) {
|
|
|
1529
1567
|
}
|
|
1530
1568
|
} else if (item.type === "split-group") {
|
|
1531
1569
|
regularContainer.appendChild(renderSplitGroupItem(item.group, item.members));
|
|
1570
|
+
} else if (item.type === "driver-hierarchy") {
|
|
1571
|
+
regularContainer.appendChild(renderDesktopDriverHierarchy(item.root, renderSessionItem, function () { renderSessionList(null); }, searchMatchIds));
|
|
1572
|
+
} else if (item.type === "orphan-workers") {
|
|
1573
|
+
regularContainer.appendChild(renderDesktopOrphanHierarchy(item.workers, renderSessionItem, function () { renderSessionList(null); }, searchMatchIds));
|
|
1532
1574
|
} else {
|
|
1533
1575
|
regularContainer.appendChild(renderSessionItem(item.data));
|
|
1534
1576
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function workerProposalSelection(msg) {
|
|
2
|
+
var hasSelection = typeof msg.selectedVendor === "string" ||
|
|
3
|
+
typeof msg.selectedModel === "string" || typeof msg.selectedEffort === "string";
|
|
4
|
+
return {
|
|
5
|
+
selected: hasSelection,
|
|
6
|
+
vendor: hasSelection ? (msg.selectedVendor || "") : (msg.recommendedVendor || ""),
|
|
7
|
+
model: hasSelection ? (msg.selectedModel || "") : (msg.recommendedModel || ""),
|
|
8
|
+
effort: hasSelection ? (msg.selectedEffort || "") : (msg.recommendedEffort || "medium"),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function syncWorkerProposalSelection(msg, sync) {
|
|
13
|
+
var selection = workerProposalSelection(msg);
|
|
14
|
+
if (selection.selected && typeof sync === "function") sync(selection);
|
|
15
|
+
return selection;
|
|
16
|
+
}
|
|
@@ -2,6 +2,7 @@ import { getWs } from './ws-ref.js';
|
|
|
2
2
|
import { addToMessages, scrollToBottom, VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
|
|
3
3
|
import { effortLevelsFor, effortDisplayName } from './app-panels.js';
|
|
4
4
|
import { iconHtml, refreshIcons } from './icons.js';
|
|
5
|
+
import { workerProposalSelection, syncWorkerProposalSelection } from './worker-proposal-state.js';
|
|
5
6
|
|
|
6
7
|
function optionValue(entry) {
|
|
7
8
|
if (typeof entry === "string") return entry;
|
|
@@ -27,7 +28,7 @@ function planSteps(plan) {
|
|
|
27
28
|
}).filter(Boolean).slice(0, 8);
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
function buildVendorSelect(msg) {
|
|
31
|
+
function buildVendorSelect(msg, preferred) {
|
|
31
32
|
var select = document.createElement("select");
|
|
32
33
|
select.className = "worker-proposal-select worker-proposal-vendor";
|
|
33
34
|
var installed = (msg.options && msg.options.installedVendors) || [];
|
|
@@ -37,7 +38,7 @@ function buildVendorSelect(msg) {
|
|
|
37
38
|
option.textContent = VENDOR_NAMES[installed[i]] || installed[i];
|
|
38
39
|
select.appendChild(option);
|
|
39
40
|
}
|
|
40
|
-
if (installed.indexOf(
|
|
41
|
+
if (installed.indexOf(preferred) !== -1) select.value = preferred;
|
|
41
42
|
return select;
|
|
42
43
|
}
|
|
43
44
|
|
|
@@ -98,16 +99,30 @@ function statusLabel(status) {
|
|
|
98
99
|
if (status === "interrupted") return "Interrupted";
|
|
99
100
|
if (status === "declined") return "Continuing here";
|
|
100
101
|
if (status === "error") return "Needs attention";
|
|
101
|
-
return "
|
|
102
|
+
return "Awaiting your choice";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function decisionLabel(status, autoAccepted) {
|
|
106
|
+
if (autoAccepted) return "Driver recommendation auto-accepted under Full auto";
|
|
107
|
+
if (status === "pending") return "Runtime choice remains with you";
|
|
108
|
+
if (status === "declined") return "Runtime proposal declined by user";
|
|
109
|
+
return "Runtime configuration accepted by user";
|
|
102
110
|
}
|
|
103
111
|
|
|
104
112
|
function applyState(card, msg) {
|
|
105
113
|
var status = msg.status || "pending";
|
|
106
114
|
card.dataset.status = status;
|
|
107
|
-
if (msg.autoApproved) card.dataset.
|
|
108
|
-
|
|
115
|
+
if (msg.autoAccepted || msg.autoApproved) card.dataset.autoAccepted = "true";
|
|
116
|
+
if (msg.autoAccepted === false || msg.autoApproved === false) card.dataset.autoAccepted = "false";
|
|
117
|
+
var autoAccepted = card.dataset.autoAccepted === "true";
|
|
118
|
+
syncWorkerProposalSelection(msg, card._syncWorkerProposalSelection);
|
|
109
119
|
var badge = card.querySelector(".worker-proposal-status");
|
|
110
|
-
if (badge) badge.textContent = statusLabel(status) + (
|
|
120
|
+
if (badge) badge.textContent = statusLabel(status) + (autoAccepted ? " · auto-accepted" : "");
|
|
121
|
+
var decision = card.querySelector(".worker-proposal-decision");
|
|
122
|
+
if (decision) {
|
|
123
|
+
decision.textContent = decisionLabel(status, autoAccepted);
|
|
124
|
+
decision.classList.toggle("automatic", autoAccepted);
|
|
125
|
+
}
|
|
111
126
|
var error = card.querySelector(".worker-proposal-error");
|
|
112
127
|
if (error) {
|
|
113
128
|
error.textContent = msg.error || "";
|
|
@@ -120,7 +135,7 @@ function applyState(card, msg) {
|
|
|
120
135
|
}
|
|
121
136
|
setControlsDisabled(card, status !== "pending");
|
|
122
137
|
var actions = card.querySelector(".worker-proposal-actions");
|
|
123
|
-
if (actions) actions.classList.toggle("hidden",
|
|
138
|
+
if (actions) actions.classList.toggle("hidden", autoAccepted);
|
|
124
139
|
}
|
|
125
140
|
|
|
126
141
|
function sendDecision(card, accepted) {
|
|
@@ -150,10 +165,12 @@ export function renderWorkerProposal(msg) {
|
|
|
150
165
|
card.className = "worker-proposal-card";
|
|
151
166
|
card.dataset.proposalId = msg.proposalId;
|
|
152
167
|
card.dataset.status = msg.status || "pending";
|
|
168
|
+
var runtimeSelection = workerProposalSelection(msg);
|
|
153
169
|
|
|
154
170
|
var header = document.createElement("div");
|
|
155
171
|
header.className = "worker-proposal-header";
|
|
156
|
-
|
|
172
|
+
var replacing = msg.action === "replace";
|
|
173
|
+
header.innerHTML = '<div class="worker-proposal-mark">' + iconHtml("git-branch") + '</div><div class="worker-proposal-heading"><span class="worker-proposal-kicker">DRIVER / SPLIT WORKER</span><strong>' + (replacing ? "Configure a replacement Split Worker" : "Configure a Split Worker") + '</strong></div><span class="worker-proposal-status"></span>';
|
|
157
174
|
card.appendChild(header);
|
|
158
175
|
|
|
159
176
|
var summary = document.createElement("p");
|
|
@@ -173,12 +190,24 @@ export function renderWorkerProposal(msg) {
|
|
|
173
190
|
card.appendChild(plan);
|
|
174
191
|
}
|
|
175
192
|
|
|
193
|
+
if (msg.recommendationRationale) {
|
|
194
|
+
var rationale = document.createElement("div");
|
|
195
|
+
rationale.className = "worker-proposal-rationale";
|
|
196
|
+
var rationaleLabel = document.createElement("span");
|
|
197
|
+
rationaleLabel.textContent = "Driver's recommendation rationale";
|
|
198
|
+
var rationaleText = document.createElement("p");
|
|
199
|
+
rationaleText.textContent = msg.recommendationRationale;
|
|
200
|
+
rationale.appendChild(rationaleLabel);
|
|
201
|
+
rationale.appendChild(rationaleText);
|
|
202
|
+
card.appendChild(rationale);
|
|
203
|
+
}
|
|
204
|
+
|
|
176
205
|
var config = document.createElement("div");
|
|
177
206
|
config.className = "worker-proposal-config";
|
|
178
207
|
var vendorField = document.createElement("label");
|
|
179
208
|
vendorField.className = "worker-proposal-field worker-proposal-vendor-field";
|
|
180
209
|
vendorField.innerHTML = '<span>Worker</span><span class="worker-proposal-vendor-control"><img alt=""></span>';
|
|
181
|
-
var vendorSelect = buildVendorSelect(msg);
|
|
210
|
+
var vendorSelect = buildVendorSelect(msg, runtimeSelection.vendor);
|
|
182
211
|
vendorField.querySelector(".worker-proposal-vendor-control").appendChild(vendorSelect);
|
|
183
212
|
config.appendChild(vendorField);
|
|
184
213
|
var modelField = document.createElement("label");
|
|
@@ -194,6 +223,10 @@ export function renderWorkerProposal(msg) {
|
|
|
194
223
|
config.appendChild(effortField);
|
|
195
224
|
card.appendChild(config);
|
|
196
225
|
|
|
226
|
+
var decision = document.createElement("div");
|
|
227
|
+
decision.className = "worker-proposal-decision";
|
|
228
|
+
card.appendChild(decision);
|
|
229
|
+
|
|
197
230
|
function syncVendor(preferredModel, preferredEffort) {
|
|
198
231
|
var vendor = vendorSelect.value;
|
|
199
232
|
var avatar = vendorField.querySelector("img");
|
|
@@ -201,12 +234,16 @@ export function renderWorkerProposal(msg) {
|
|
|
201
234
|
fillModels(modelSelect, vendor, msg, preferredModel);
|
|
202
235
|
fillEffort(card, vendor, modelSelect.value, msg, preferredEffort);
|
|
203
236
|
}
|
|
237
|
+
card._syncWorkerProposalSelection = function (selection) {
|
|
238
|
+
if (selection.vendor && vendorSelect.value !== selection.vendor) vendorSelect.value = selection.vendor;
|
|
239
|
+
syncVendor(selection.model, selection.effort);
|
|
240
|
+
};
|
|
204
241
|
vendorSelect.addEventListener("change", function () { syncVendor("", "medium"); });
|
|
205
242
|
modelSelect.addEventListener("change", function () {
|
|
206
243
|
var active = card.querySelector(".worker-proposal-effort-btn.active");
|
|
207
244
|
fillEffort(card, vendorSelect.value, modelSelect.value, msg, active ? active.dataset.effort : "medium");
|
|
208
245
|
});
|
|
209
|
-
syncVendor(
|
|
246
|
+
syncVendor(runtimeSelection.model, runtimeSelection.effort);
|
|
210
247
|
|
|
211
248
|
var error = document.createElement("div");
|
|
212
249
|
error.className = "worker-proposal-error hidden";
|
|
@@ -220,12 +257,12 @@ export function renderWorkerProposal(msg) {
|
|
|
220
257
|
var decline = document.createElement("button");
|
|
221
258
|
decline.type = "button";
|
|
222
259
|
decline.className = "worker-proposal-action secondary";
|
|
223
|
-
decline.textContent = "Continue here";
|
|
260
|
+
decline.textContent = replacing ? "Keep current Worker" : "Continue here";
|
|
224
261
|
decline.addEventListener("click", function () { sendDecision(card, false); });
|
|
225
262
|
var accept = document.createElement("button");
|
|
226
263
|
accept.type = "button";
|
|
227
264
|
accept.className = "worker-proposal-action primary";
|
|
228
|
-
accept.innerHTML = iconHtml("panel-right-open") + "<span>Run with Split Worker</span>";
|
|
265
|
+
accept.innerHTML = iconHtml("panel-right-open") + "<span>" + (replacing ? "Replace Split Worker" : "Run with Split Worker") + "</span>";
|
|
229
266
|
accept.addEventListener("click", function () { sendDecision(card, true); });
|
|
230
267
|
actions.appendChild(decline);
|
|
231
268
|
actions.appendChild(accept);
|
package/lib/sdk-bridge.js
CHANGED
|
@@ -605,14 +605,12 @@ function createSDKBridge(opts) {
|
|
|
605
605
|
return { behavior: "allow", updatedInput: input };
|
|
606
606
|
}
|
|
607
607
|
|
|
608
|
-
// Auto-approve the
|
|
609
|
-
// Driver's answer to a Worker
|
|
610
|
-
//
|
|
611
|
-
//
|
|
612
|
-
//
|
|
613
|
-
//
|
|
614
|
-
// A prompt on each hop would reinstate exactly the approval gate the
|
|
615
|
-
// autonomous lifecycle exists to remove.
|
|
608
|
+
// Auto-approve the non-mutating configuration proposal and the visible
|
|
609
|
+
// Driver/Split Worker pair tools, including the Driver's answer to a Worker
|
|
610
|
+
// permission request. Creation and replacement still require a separate
|
|
611
|
+
// human decision in the proposal card; skip-permissions is never treated as
|
|
612
|
+
// that product choice. Every handler is confined to the caller's exact
|
|
613
|
+
// session/pair and re-checks eligibility, ownership and live identity.
|
|
616
614
|
//
|
|
617
615
|
// spawn_sessions is deliberately absent: it creates arbitrary sessions.
|
|
618
616
|
//
|
|
@@ -628,6 +626,7 @@ function createSDKBridge(opts) {
|
|
|
628
626
|
// can legitimately arrive bare.
|
|
629
627
|
var PAIR_MCP_PREFIX = "mcp__clay-sessions__";
|
|
630
628
|
var pairMcpTools = {
|
|
629
|
+
propose_worker: true,
|
|
631
630
|
send_to_partner: true,
|
|
632
631
|
read_partner: true,
|
|
633
632
|
partner_status: true,
|
|
@@ -638,6 +637,7 @@ function createSDKBridge(opts) {
|
|
|
638
637
|
respond_to_worker_permission: true,
|
|
639
638
|
};
|
|
640
639
|
var pairDynamicTools = {
|
|
640
|
+
propose_worker: true,
|
|
641
641
|
partner_status: true,
|
|
642
642
|
replace_partner: true,
|
|
643
643
|
interrupt_partner: true,
|
|
@@ -664,6 +664,14 @@ function createSDKBridge(opts) {
|
|
|
664
664
|
return { behavior: "allow", updatedInput: input };
|
|
665
665
|
}
|
|
666
666
|
|
|
667
|
+
// Project Logs are exact-session, project-bound tools whose authority is
|
|
668
|
+
// enforced again by the server binding. Canonical changes are append-only
|
|
669
|
+
// revisions, and the surface exposes no destructive delete operation, so
|
|
670
|
+
// a second provider permission prompt adds no additional safety.
|
|
671
|
+
if (toolName.indexOf("mcp__clay-logs__") === 0) {
|
|
672
|
+
return { behavior: "allow", updatedInput: input };
|
|
673
|
+
}
|
|
674
|
+
|
|
667
675
|
// This tool only prepares a read-only UI projection for the document the
|
|
668
676
|
// agent is about to edit. The subsequent Edit/Write keeps its own normal
|
|
669
677
|
// permission behavior.
|
|
@@ -3,6 +3,8 @@ var yoke = require("./yoke");
|
|
|
3
3
|
var backgroundTaskTiming = require("./background-task-timing");
|
|
4
4
|
var sessionTitlePolicy = require("./session-title-policy");
|
|
5
5
|
|
|
6
|
+
var OVERLOAD_RETRY_LIMIT = 3;
|
|
7
|
+
|
|
6
8
|
function attachMessageProcessor(ctx) {
|
|
7
9
|
var sm = ctx.sm;
|
|
8
10
|
var send = ctx.send;
|
|
@@ -487,6 +489,8 @@ function attachMessageProcessor(ctx) {
|
|
|
487
489
|
}
|
|
488
490
|
|
|
489
491
|
} else if (parsed.yokeType === "result") {
|
|
492
|
+
var overloadAbortRequested = !!session._overloadAbortRequested;
|
|
493
|
+
session._overloadAbortRequested = false;
|
|
490
494
|
session.blocks = {};
|
|
491
495
|
session.sentToolResults = {};
|
|
492
496
|
session.pendingPermissions = {};
|
|
@@ -532,7 +536,9 @@ function attachMessageProcessor(ctx) {
|
|
|
532
536
|
session._awaitingTurnResult = false;
|
|
533
537
|
session._queuedTurnCount = 0;
|
|
534
538
|
onProcessingChanged();
|
|
535
|
-
|
|
539
|
+
if (!overloadAbortRequested) {
|
|
540
|
+
sendAndRecord(session, { type: "error", text: "Claude error: " + execError });
|
|
541
|
+
}
|
|
536
542
|
sendAndRecord(session, { type: "done", code: 1 });
|
|
537
543
|
sm.broadcastSessionList();
|
|
538
544
|
return;
|
|
@@ -819,9 +825,33 @@ function attachMessageProcessor(ctx) {
|
|
|
819
825
|
}
|
|
820
826
|
|
|
821
827
|
} else if (parsed.yokeType === "api_retry") {
|
|
822
|
-
//
|
|
823
|
-
|
|
824
|
-
|
|
828
|
+
// The Claude SDK may otherwise paint one persistent red error for every
|
|
829
|
+
// retry and remain busy for many minutes. Show one transient status, and
|
|
830
|
+
// stop a repeatedly overloaded turn after a small bounded number of
|
|
831
|
+
// attempts so the user regains control and can retry or change models.
|
|
832
|
+
var apiRetryAttempt = parsed.attempt || 1;
|
|
833
|
+
if (parsed.error === "overloaded") {
|
|
834
|
+
if (apiRetryAttempt === 1) {
|
|
835
|
+
session._overloadAbortRequested = false;
|
|
836
|
+
sendToSession(session, {
|
|
837
|
+
type: "system_info",
|
|
838
|
+
text: "The model is overloaded. Retrying briefly...",
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
if (apiRetryAttempt >= OVERLOAD_RETRY_LIMIT && !session._overloadAbortRequested) {
|
|
842
|
+
session._overloadAbortRequested = true;
|
|
843
|
+
sendAndRecord(session, {
|
|
844
|
+
type: "error",
|
|
845
|
+
text: "The model remained overloaded after " + apiRetryAttempt +
|
|
846
|
+
" attempts, so this turn was stopped. Try again or choose another model.",
|
|
847
|
+
});
|
|
848
|
+
if (session.abortController && typeof session.abortController.abort === "function") {
|
|
849
|
+
try { session.abortController.abort(); } catch (e) {}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
} else if (apiRetryAttempt === 1) {
|
|
853
|
+
sendToSession(session, { type: "system_info", text: "The model request failed temporarily. Retrying..." });
|
|
854
|
+
}
|
|
825
855
|
|
|
826
856
|
} else if (parsed.yokeType === "commands_changed") {
|
|
827
857
|
// Mid-session command list change: rebuild the union with skills (same as
|