clay-server 3.4.0-beta.9 → 3.4.0
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/daemon.js +58 -58
- package/lib/project-connection.js +19 -6
- package/lib/project-models.js +220 -0
- package/lib/project-session-handoff.js +162 -0
- package/lib/project-session-pair.js +14 -7
- package/lib/project-session-spawn.js +1 -1
- package/lib/project-sessions.js +13 -45
- package/lib/project-worker-proposal.js +51 -14
- package/lib/project.js +33 -77
- package/lib/public/app.js +11 -0
- package/lib/public/copilot-avatar.svg +5 -0
- package/lib/public/css/filebrowser.css +6 -1
- package/lib/public/css/input.css +56 -0
- package/lib/public/css/mates.css +6 -0
- package/lib/public/css/menus.css +35 -56
- package/lib/public/css/pane.css +18 -3
- package/lib/public/css/session-actions.css +98 -0
- package/lib/public/grok-avatar.svg +4 -0
- package/lib/public/index.html +26 -9
- package/lib/public/junie-avatar.svg +11 -0
- package/lib/public/kimi-avatar.svg +4 -0
- package/lib/public/modules/agent-config-selects.js +69 -0
- package/lib/public/modules/app-connection.js +6 -0
- package/lib/public/modules/app-header.js +0 -22
- package/lib/public/modules/app-messages.js +48 -9
- package/lib/public/modules/app-panels.js +33 -82
- package/lib/public/modules/app-projects.js +2 -0
- package/lib/public/modules/app-rendering.js +16 -1
- package/lib/public/modules/background-tasks-ui.js +55 -0
- package/lib/public/modules/filebrowser-tabs.js +65 -0
- package/lib/public/modules/filebrowser.js +23 -3
- package/lib/public/modules/mate-sidebar.js +10 -2
- package/lib/public/modules/model-picker.js +263 -0
- package/lib/public/modules/pane-bridge.js +6 -0
- package/lib/public/modules/session-actions.js +294 -0
- package/lib/public/modules/sidebar-mobile.js +2 -1
- package/lib/public/modules/sidebar-sessions.js +17 -1
- package/lib/public/modules/split-pair-ui.js +16 -77
- package/lib/public/modules/split-view.js +11 -1
- package/lib/public/modules/tools.js +5 -0
- package/lib/public/modules/vendor-priority.js +6 -1
- package/lib/public/modules/worker-proposal.js +6 -1
- package/lib/public/qwen-avatar.svg +4 -0
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +57 -34
- package/lib/sdk-message-processor.js +26 -4
- package/lib/session-handoff-context.js +110 -0
- package/lib/session-notes-mcp-server.js +1 -0
- package/lib/sessions.js +4 -0
- package/lib/ws-schema.js +9 -1
- package/lib/yoke/acp-agent-profiles.js +64 -0
- package/lib/yoke/adapters/acp.js +2 -1
- package/lib/yoke/adapters/claude.js +34 -0
- package/lib/yoke/adapters/codex.js +38 -3
- package/lib/yoke/adapters/copilot.js +7 -0
- package/lib/yoke/adapters/grok.js +7 -0
- package/lib/yoke/adapters/junie.js +7 -0
- package/lib/yoke/adapters/kimi.js +7 -0
- package/lib/yoke/adapters/kiro.js +4 -3
- package/lib/yoke/adapters/qwen.js +7 -0
- package/lib/yoke/codex-background-tasks.js +84 -0
- package/lib/yoke/index.js +43 -14
- package/lib/yoke/instructions.js +3 -0
- package/lib/yoke/interface.js +21 -0
- package/lib/yoke/vendor-registry.js +55 -0
- package/package.json +1 -1
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
|
|
4
|
+
var configModelList = null;
|
|
5
|
+
var configPopover = null;
|
|
6
|
+
var configChip = null;
|
|
7
|
+
var requestCounter = 0;
|
|
8
|
+
var selectionCounter = 0;
|
|
9
|
+
var requestTimer = null;
|
|
10
|
+
|
|
11
|
+
export function modelEntryValue(entry) {
|
|
12
|
+
if (!entry) return "";
|
|
13
|
+
if (typeof entry === "string") return entry;
|
|
14
|
+
return entry.value || entry.id || "";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function modelEntryMatches(entry, value) {
|
|
18
|
+
if (!entry || !value) return false;
|
|
19
|
+
if (typeof entry === "string") return entry === value;
|
|
20
|
+
return entry.value === value || entry.id === value || entry.resolvedModel === value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function modelDisplayName(value, models) {
|
|
24
|
+
if (!value) return "";
|
|
25
|
+
var list = models || [];
|
|
26
|
+
for (var i = 0; i < list.length; i++) {
|
|
27
|
+
if (modelEntryMatches(list[i], value)) {
|
|
28
|
+
return typeof list[i] === "string" ? list[i] : (list[i].displayName || modelEntryValue(list[i]));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function vendorDisplayName(vendor) {
|
|
35
|
+
var info = store.get('vendorInfo') || {};
|
|
36
|
+
return info[vendor] && info[vendor].displayName ? info[vendor].displayName : vendor;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function clearRequestTimer() {
|
|
40
|
+
if (!requestTimer) return;
|
|
41
|
+
clearTimeout(requestTimer);
|
|
42
|
+
requestTimer = null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function setupModelPicker() {
|
|
46
|
+
configModelList = document.getElementById("config-model-list");
|
|
47
|
+
configPopover = document.getElementById("config-popover");
|
|
48
|
+
configChip = document.getElementById("config-chip");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resetModelPickerState() {
|
|
52
|
+
clearRequestTimer();
|
|
53
|
+
store.set({
|
|
54
|
+
modelListVendor: "",
|
|
55
|
+
modelListStatus: "idle",
|
|
56
|
+
modelListError: "",
|
|
57
|
+
modelRequestId: "",
|
|
58
|
+
modelSelectionPending: null,
|
|
59
|
+
modelSelectionError: "",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function requestVendorModels(vendor, force) {
|
|
64
|
+
if (!vendor) return null;
|
|
65
|
+
var s = store.snap();
|
|
66
|
+
if (!force && s.modelListVendor === vendor) {
|
|
67
|
+
if (s.modelListStatus === "loading") return s.modelRequestId || null;
|
|
68
|
+
if (s.modelListStatus === "ready" && s.currentModels && s.currentModels.length > 0) return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
var ws = getWs();
|
|
72
|
+
if (!ws || ws.readyState !== 1) {
|
|
73
|
+
store.set({
|
|
74
|
+
modelListVendor: vendor,
|
|
75
|
+
modelListStatus: "error",
|
|
76
|
+
modelListError: "The connection is not ready. Reconnect and retry.",
|
|
77
|
+
});
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
requestCounter++;
|
|
82
|
+
var requestId = "models-" + Date.now().toString(36) + "-" + requestCounter;
|
|
83
|
+
clearRequestTimer();
|
|
84
|
+
store.set({
|
|
85
|
+
modelListVendor: vendor,
|
|
86
|
+
modelListStatus: "loading",
|
|
87
|
+
modelListError: "",
|
|
88
|
+
modelRequestId: requestId,
|
|
89
|
+
});
|
|
90
|
+
ws.send(JSON.stringify({ type: "get_vendor_models", vendor: vendor, requestId: requestId }));
|
|
91
|
+
requestTimer = setTimeout(function() {
|
|
92
|
+
var current = store.snap();
|
|
93
|
+
if (current.modelRequestId !== requestId || current.modelListStatus !== "loading") return;
|
|
94
|
+
store.set({
|
|
95
|
+
modelListStatus: "error",
|
|
96
|
+
modelListError: vendorDisplayName(vendor) + " model loading timed out. Retry to try again.",
|
|
97
|
+
});
|
|
98
|
+
}, 35000);
|
|
99
|
+
return requestId;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function prepareModelPickerOpen() {
|
|
103
|
+
var s = store.snap();
|
|
104
|
+
var vendor = s.currentVendor || "claude";
|
|
105
|
+
var needsModels = !s.currentModels || s.currentModels.length === 0;
|
|
106
|
+
var wrongVendor = s.modelListVendor !== vendor;
|
|
107
|
+
var retryable = s.modelListStatus === "error" || s.modelListStatus === "empty" || s.modelListStatus === "idle";
|
|
108
|
+
requestVendorModels(vendor, needsModels || wrongVendor || retryable);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function getModelInfoUpdate(msg) {
|
|
112
|
+
var vendor = msg.vendor || store.get('currentVendor') || "claude";
|
|
113
|
+
var currentVendor = store.get('currentVendor');
|
|
114
|
+
if (currentVendor && vendor !== currentVendor) return null;
|
|
115
|
+
if (msg.sessionId != null && msg.sessionId !== store.get('activeSessionId')) return null;
|
|
116
|
+
|
|
117
|
+
var s = store.snap();
|
|
118
|
+
if (msg.requestId && s.modelRequestId && msg.requestId !== s.modelRequestId) return null;
|
|
119
|
+
|
|
120
|
+
var models = Array.isArray(msg.models) ? msg.models : [];
|
|
121
|
+
var unsolicitedEmptyWhileLoading = s.modelListStatus === "loading"
|
|
122
|
+
&& s.modelListVendor === vendor
|
|
123
|
+
&& !msg.requestId
|
|
124
|
+
&& !msg.modelStatus
|
|
125
|
+
&& !msg.error
|
|
126
|
+
&& models.length === 0;
|
|
127
|
+
if (unsolicitedEmptyWhileLoading) {
|
|
128
|
+
return {
|
|
129
|
+
modelListVendor: vendor,
|
|
130
|
+
modelListStatus: "loading",
|
|
131
|
+
modelListError: "",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
clearRequestTimer();
|
|
136
|
+
var status = msg.modelStatus || (msg.error ? "error" : (models.length > 0 ? "ready" : "empty"));
|
|
137
|
+
return {
|
|
138
|
+
modelListVendor: vendor,
|
|
139
|
+
modelListStatus: status,
|
|
140
|
+
modelListError: msg.error || "",
|
|
141
|
+
modelRequestId: "",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function requestModelSelection(model) {
|
|
146
|
+
var ws = getWs();
|
|
147
|
+
if (!ws || ws.readyState !== 1) {
|
|
148
|
+
store.set({ modelSelectionError: "The connection is not ready. Reconnect before selecting a model." });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
var s = store.snap();
|
|
152
|
+
selectionCounter++;
|
|
153
|
+
var requestId = "model-select-" + Date.now().toString(36) + "-" + selectionCounter;
|
|
154
|
+
store.set({
|
|
155
|
+
currentModel: model,
|
|
156
|
+
modelSelectionPending: { requestId: requestId, model: model, previousModel: s.currentModel || "" },
|
|
157
|
+
modelSelectionError: "",
|
|
158
|
+
});
|
|
159
|
+
ws.send(JSON.stringify({
|
|
160
|
+
type: "set_model",
|
|
161
|
+
model: model,
|
|
162
|
+
vendor: s.currentVendor || "claude",
|
|
163
|
+
requestId: requestId,
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function handleModelSelectionResult(msg) {
|
|
168
|
+
var pending = store.get('modelSelectionPending');
|
|
169
|
+
if (!pending || (msg.requestId && msg.requestId !== pending.requestId)) return false;
|
|
170
|
+
if (msg.ok) {
|
|
171
|
+
store.set({
|
|
172
|
+
currentModel: msg.model || pending.model,
|
|
173
|
+
modelSelectionPending: null,
|
|
174
|
+
modelSelectionError: "",
|
|
175
|
+
});
|
|
176
|
+
if (configPopover) configPopover.classList.add("hidden");
|
|
177
|
+
if (configChip) configChip.classList.remove("active");
|
|
178
|
+
} else {
|
|
179
|
+
store.set({
|
|
180
|
+
currentModel: pending.previousModel,
|
|
181
|
+
modelSelectionPending: null,
|
|
182
|
+
modelSelectionError: msg.error || "The model could not be selected.",
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function appendState(title, detail, retry) {
|
|
189
|
+
var stateEl = document.createElement("div");
|
|
190
|
+
stateEl.className = "config-model-state";
|
|
191
|
+
if (title === "Loading models…") {
|
|
192
|
+
var spinner = document.createElement("span");
|
|
193
|
+
spinner.className = "config-model-spinner";
|
|
194
|
+
spinner.setAttribute("aria-hidden", "true");
|
|
195
|
+
stateEl.appendChild(spinner);
|
|
196
|
+
}
|
|
197
|
+
var textWrap = document.createElement("div");
|
|
198
|
+
var titleEl = document.createElement("div");
|
|
199
|
+
titleEl.className = "config-model-state-title";
|
|
200
|
+
titleEl.textContent = title;
|
|
201
|
+
textWrap.appendChild(titleEl);
|
|
202
|
+
if (detail) {
|
|
203
|
+
var detailEl = document.createElement("div");
|
|
204
|
+
detailEl.className = "config-model-state-detail";
|
|
205
|
+
detailEl.textContent = detail;
|
|
206
|
+
textWrap.appendChild(detailEl);
|
|
207
|
+
}
|
|
208
|
+
stateEl.appendChild(textWrap);
|
|
209
|
+
if (retry) {
|
|
210
|
+
var retryBtn = document.createElement("button");
|
|
211
|
+
retryBtn.type = "button";
|
|
212
|
+
retryBtn.className = "config-model-retry";
|
|
213
|
+
retryBtn.textContent = "Retry";
|
|
214
|
+
retryBtn.addEventListener("click", function() {
|
|
215
|
+
requestVendorModels(store.get('currentVendor') || "claude", true);
|
|
216
|
+
});
|
|
217
|
+
stateEl.appendChild(retryBtn);
|
|
218
|
+
}
|
|
219
|
+
configModelList.appendChild(stateEl);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function renderModelPicker() {
|
|
223
|
+
if (!configModelList) return;
|
|
224
|
+
var s = store.snap();
|
|
225
|
+
var vendor = s.currentVendor || "claude";
|
|
226
|
+
var status = s.modelListVendor === vendor ? s.modelListStatus : "idle";
|
|
227
|
+
configModelList.innerHTML = "";
|
|
228
|
+
|
|
229
|
+
if (status === "loading") {
|
|
230
|
+
appendState("Loading models…", "Starting " + vendorDisplayName(vendor) + " and requesting its model catalog.", false);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (status === "error") {
|
|
234
|
+
appendState("Couldn’t load models", s.modelListError || "An unexpected error occurred.", true);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (status === "empty" || !s.currentModels || s.currentModels.length === 0) {
|
|
238
|
+
appendState("No models available", s.modelListError || "The vendor returned an empty model catalog.", true);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
for (var i = 0; i < s.currentModels.length; i++) {
|
|
243
|
+
var item = s.currentModels[i];
|
|
244
|
+
var value = modelEntryValue(item);
|
|
245
|
+
if (!value) continue;
|
|
246
|
+
var button = document.createElement("button");
|
|
247
|
+
button.className = "config-radio-item";
|
|
248
|
+
if (modelEntryMatches(item, s.currentModel)) button.classList.add("active");
|
|
249
|
+
button.dataset.model = value;
|
|
250
|
+
button.textContent = typeof item === "string" ? item : (item.displayName || value);
|
|
251
|
+
if (s.modelSelectionPending && s.modelSelectionPending.model === value) {
|
|
252
|
+
button.classList.add("pending");
|
|
253
|
+
button.textContent += " · Selecting…";
|
|
254
|
+
}
|
|
255
|
+
button.disabled = !!s.modelSelectionPending;
|
|
256
|
+
button.addEventListener("click", function() { requestModelSelection(this.dataset.model); });
|
|
257
|
+
configModelList.appendChild(button);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (s.modelSelectionError) {
|
|
261
|
+
appendState("Model selection failed", s.modelSelectionError, false);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -23,6 +23,12 @@ export function reportPaneContext(data) {
|
|
|
23
23
|
}, window.location.origin);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export function forwardPaneMarkdownPresentation(message) {
|
|
27
|
+
if (!store.get('paneMode') || window.parent === window) return false;
|
|
28
|
+
window.parent.postMessage({ type: "clay-pane-present-markdown", message: message }, window.location.origin);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
// Toggle without setContextView: panes share localStorage with the main app,
|
|
27
33
|
// so persisting here would silently rewrite the user's main-view preference.
|
|
28
34
|
function togglePaneContextPanel() {
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
import { refreshIcons, iconHtml } from './icons.js';
|
|
4
|
+
import { addSystemMessage, VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
|
|
5
|
+
import { openPairDialog } from './split-pair-ui.js';
|
|
6
|
+
import { buildAgentVendorSelect, fillAgentModels, buildAgentEffortSelect, fillAgentEffort } from './agent-config-selects.js';
|
|
7
|
+
import { showToast } from './utils.js';
|
|
8
|
+
|
|
9
|
+
var menu = null;
|
|
10
|
+
var button = null;
|
|
11
|
+
var outsideHandler = null;
|
|
12
|
+
var escapeHandler = null;
|
|
13
|
+
var handoffDialog = null;
|
|
14
|
+
var handoffEscapeHandler = null;
|
|
15
|
+
var handoffSubmitButton = null;
|
|
16
|
+
|
|
17
|
+
function closeMenu() {
|
|
18
|
+
if (menu) menu.remove();
|
|
19
|
+
menu = null;
|
|
20
|
+
if (button) button.setAttribute("aria-expanded", "false");
|
|
21
|
+
if (outsideHandler) document.removeEventListener("pointerdown", outsideHandler, true);
|
|
22
|
+
if (escapeHandler) document.removeEventListener("keydown", escapeHandler);
|
|
23
|
+
outsideHandler = null;
|
|
24
|
+
escapeHandler = null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function positionMenu() {
|
|
28
|
+
if (!menu || !button) return;
|
|
29
|
+
var rect = button.getBoundingClientRect();
|
|
30
|
+
var width = menu.offsetWidth;
|
|
31
|
+
menu.style.top = Math.round(rect.bottom + 6) + "px";
|
|
32
|
+
menu.style.left = Math.max(8, Math.round(rect.right - width)) + "px";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function actionRow(icon, label, description) {
|
|
36
|
+
var row = document.createElement("button");
|
|
37
|
+
row.type = "button";
|
|
38
|
+
row.className = "session-actions-row";
|
|
39
|
+
row.innerHTML = iconHtml(icon, "session-actions-row-icon") +
|
|
40
|
+
'<span class="session-actions-row-copy"><strong></strong><span></span></span>' +
|
|
41
|
+
iconHtml("chevron-right", "session-actions-row-chevron");
|
|
42
|
+
row.querySelector("strong").textContent = label;
|
|
43
|
+
row.querySelector(".session-actions-row-copy > span").textContent = description;
|
|
44
|
+
return row;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function menuShell(label) {
|
|
48
|
+
var el = document.createElement("div");
|
|
49
|
+
el.className = "session-actions-menu";
|
|
50
|
+
el.setAttribute("role", "menu");
|
|
51
|
+
el.setAttribute("aria-label", label);
|
|
52
|
+
document.body.appendChild(el);
|
|
53
|
+
menu = el;
|
|
54
|
+
button.setAttribute("aria-expanded", "true");
|
|
55
|
+
return el;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function installCloseHandlers() {
|
|
59
|
+
outsideHandler = function (event) {
|
|
60
|
+
if (menu && !menu.contains(event.target) && event.target !== button && !button.contains(event.target)) closeMenu();
|
|
61
|
+
};
|
|
62
|
+
escapeHandler = function (event) {
|
|
63
|
+
if (event.key === "Escape") {
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
closeMenu();
|
|
66
|
+
if (button) button.focus();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
setTimeout(function () {
|
|
70
|
+
document.addEventListener("pointerdown", outsideHandler, true);
|
|
71
|
+
document.addEventListener("keydown", escapeHandler);
|
|
72
|
+
}, 0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function closeHandoffDialog() {
|
|
76
|
+
if (handoffDialog) handoffDialog.remove();
|
|
77
|
+
if (handoffEscapeHandler) document.removeEventListener("keydown", handoffEscapeHandler);
|
|
78
|
+
handoffDialog = null;
|
|
79
|
+
handoffEscapeHandler = null;
|
|
80
|
+
handoffSubmitButton = null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function handoffField(labelText, control) {
|
|
84
|
+
var field = document.createElement("div");
|
|
85
|
+
field.className = "handoff-modal-field";
|
|
86
|
+
var label = document.createElement("label");
|
|
87
|
+
label.className = "wt-modal-label";
|
|
88
|
+
label.textContent = labelText;
|
|
89
|
+
field.appendChild(label);
|
|
90
|
+
field.appendChild(control);
|
|
91
|
+
return field;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function requestHandoffDialog() {
|
|
95
|
+
var ws = getWs();
|
|
96
|
+
if (!ws || ws.readyState !== 1) return;
|
|
97
|
+
closeMenu();
|
|
98
|
+
ws.send(JSON.stringify({ type: "handoff_session_options" }));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function showHandoffDialog(options) {
|
|
102
|
+
closeHandoffDialog();
|
|
103
|
+
var installed = options.installedVendors || [];
|
|
104
|
+
var vendorInfo = store.get('vendorInfo') || {};
|
|
105
|
+
if (store.get('isOsUsers')) {
|
|
106
|
+
installed = installed.filter(function (vendor) {
|
|
107
|
+
return !vendorInfo[vendor] || vendorInfo[vendor].osUserIsolation !== false;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if (installed.length < 1) {
|
|
111
|
+
showToast("Install a coding agent before continuing this session.", "error");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
var currentVendor = store.get('currentVendor') || "claude";
|
|
116
|
+
var currentModel = store.get('currentModel') || "";
|
|
117
|
+
var currentEffort = store.get('currentEffort') || "";
|
|
118
|
+
var container = document.createElement("div");
|
|
119
|
+
var overlay = document.createElement("div");
|
|
120
|
+
overlay.className = "wt-modal-overlay";
|
|
121
|
+
container.appendChild(overlay);
|
|
122
|
+
var modal = document.createElement("div");
|
|
123
|
+
modal.className = "wt-modal handoff-modal";
|
|
124
|
+
modal.setAttribute("role", "dialog");
|
|
125
|
+
modal.setAttribute("aria-modal", "true");
|
|
126
|
+
modal.setAttribute("aria-labelledby", "handoff-modal-title");
|
|
127
|
+
|
|
128
|
+
var title = document.createElement("div");
|
|
129
|
+
title.id = "handoff-modal-title";
|
|
130
|
+
title.className = "wt-modal-title";
|
|
131
|
+
title.textContent = "Continue in a new agent";
|
|
132
|
+
modal.appendChild(title);
|
|
133
|
+
var desc = document.createElement("div");
|
|
134
|
+
desc.className = "handoff-modal-desc";
|
|
135
|
+
desc.textContent = "Start an independent session with a focused copy of this conversation's context.";
|
|
136
|
+
modal.appendChild(desc);
|
|
137
|
+
|
|
138
|
+
var source = document.createElement("div");
|
|
139
|
+
source.className = "handoff-modal-source";
|
|
140
|
+
var sourceAvatar = document.createElement("img");
|
|
141
|
+
sourceAvatar.src = VENDOR_AVATARS[currentVendor] || VENDOR_AVATARS.claude;
|
|
142
|
+
sourceAvatar.alt = "";
|
|
143
|
+
source.appendChild(sourceAvatar);
|
|
144
|
+
var sourceCopy = document.createElement("span");
|
|
145
|
+
var sourceTitle = document.createElement("strong");
|
|
146
|
+
sourceTitle.textContent = document.getElementById("header-title").textContent || "Current session";
|
|
147
|
+
var sourceMeta = document.createElement("span");
|
|
148
|
+
sourceMeta.textContent = "From " + (VENDOR_NAMES[currentVendor] || currentVendor);
|
|
149
|
+
sourceCopy.appendChild(sourceTitle);
|
|
150
|
+
sourceCopy.appendChild(sourceMeta);
|
|
151
|
+
source.appendChild(sourceCopy);
|
|
152
|
+
modal.appendChild(source);
|
|
153
|
+
|
|
154
|
+
var vendorSelect = buildAgentVendorSelect(installed, currentVendor);
|
|
155
|
+
var modelSelect = document.createElement("select");
|
|
156
|
+
modelSelect.className = "wt-modal-input";
|
|
157
|
+
var effortSelect = buildAgentEffortSelect();
|
|
158
|
+
var vendorField = handoffField("Agent", vendorSelect);
|
|
159
|
+
var modelField = handoffField("Model", modelSelect);
|
|
160
|
+
var effortField = handoffField("Reasoning effort", effortSelect);
|
|
161
|
+
modal.appendChild(vendorField);
|
|
162
|
+
modal.appendChild(modelField);
|
|
163
|
+
modal.appendChild(effortField);
|
|
164
|
+
|
|
165
|
+
function syncEffort(preferred) {
|
|
166
|
+
fillAgentEffort(effortSelect, vendorSelect.value, options, modelSelect.value, preferred);
|
|
167
|
+
var capabilities = (options.capabilitiesByVendor && options.capabilitiesByVendor[vendorSelect.value]) || {};
|
|
168
|
+
effortField.style.display = capabilities.effort === false ? "none" : "";
|
|
169
|
+
}
|
|
170
|
+
fillAgentModels(modelSelect, vendorSelect.value, options, vendorSelect.value === currentVendor ? currentModel : "");
|
|
171
|
+
syncEffort(vendorSelect.value === currentVendor ? currentEffort : "");
|
|
172
|
+
vendorSelect.addEventListener("change", function () {
|
|
173
|
+
fillAgentModels(modelSelect, vendorSelect.value, options, "");
|
|
174
|
+
syncEffort("");
|
|
175
|
+
});
|
|
176
|
+
modelSelect.addEventListener("change", function () { syncEffort(effortSelect.value); });
|
|
177
|
+
|
|
178
|
+
var actions = document.createElement("div");
|
|
179
|
+
actions.className = "wt-modal-actions";
|
|
180
|
+
var cancel = document.createElement("button");
|
|
181
|
+
cancel.type = "button";
|
|
182
|
+
cancel.className = "wt-modal-btn";
|
|
183
|
+
cancel.textContent = "Cancel";
|
|
184
|
+
var submit = document.createElement("button");
|
|
185
|
+
submit.type = "button";
|
|
186
|
+
submit.className = "wt-modal-btn primary";
|
|
187
|
+
submit.textContent = "Continue";
|
|
188
|
+
actions.appendChild(cancel);
|
|
189
|
+
actions.appendChild(submit);
|
|
190
|
+
modal.appendChild(actions);
|
|
191
|
+
container.appendChild(modal);
|
|
192
|
+
document.body.appendChild(container);
|
|
193
|
+
handoffDialog = container;
|
|
194
|
+
handoffSubmitButton = submit;
|
|
195
|
+
|
|
196
|
+
cancel.addEventListener("click", closeHandoffDialog);
|
|
197
|
+
overlay.addEventListener("click", closeHandoffDialog);
|
|
198
|
+
handoffEscapeHandler = function (event) {
|
|
199
|
+
if (event.key === "Escape") closeHandoffDialog();
|
|
200
|
+
};
|
|
201
|
+
document.addEventListener("keydown", handoffEscapeHandler);
|
|
202
|
+
submit.addEventListener("click", function () {
|
|
203
|
+
var ws = getWs();
|
|
204
|
+
if (!ws || ws.readyState !== 1) return;
|
|
205
|
+
ws.send(JSON.stringify({
|
|
206
|
+
type: "handoff_session",
|
|
207
|
+
targetVendor: vendorSelect.value,
|
|
208
|
+
model: modelSelect.value,
|
|
209
|
+
effort: effortSelect.value,
|
|
210
|
+
}));
|
|
211
|
+
submit.disabled = true;
|
|
212
|
+
submit.textContent = "Continuing…";
|
|
213
|
+
});
|
|
214
|
+
vendorSelect.focus();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function showMainMenu() {
|
|
218
|
+
closeMenu();
|
|
219
|
+
var el = menuShell("Session actions");
|
|
220
|
+
var addWorker = actionRow("bot", "Add AI Worker", "Open a second agent beside this session.");
|
|
221
|
+
addWorker.addEventListener("click", function () {
|
|
222
|
+
var sessionId = store.get('activeSessionId');
|
|
223
|
+
if (!sessionId) return;
|
|
224
|
+
closeMenu();
|
|
225
|
+
openPairDialog({
|
|
226
|
+
sessionId: sessionId,
|
|
227
|
+
title: document.getElementById("header-title").textContent,
|
|
228
|
+
vendor: store.get('currentVendor') || "claude",
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
var handoff = actionRow("forward", "Continue in another agent", "Carry this conversation into a new session.");
|
|
232
|
+
var unavailable = !store.get('sessionHasHistory') || store.get('sessionIsProcessing');
|
|
233
|
+
handoff.disabled = unavailable;
|
|
234
|
+
handoff.title = unavailable ? "Available after the current turn is complete" : "";
|
|
235
|
+
handoff.addEventListener("click", requestHandoffDialog);
|
|
236
|
+
el.appendChild(addWorker);
|
|
237
|
+
el.appendChild(handoff);
|
|
238
|
+
refreshIcons();
|
|
239
|
+
positionMenu();
|
|
240
|
+
installCloseHandlers();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function updateVisibility() {
|
|
244
|
+
if (!button) return;
|
|
245
|
+
var state = store.snap();
|
|
246
|
+
var hidden = !state.activeSessionId || state.dmMode || state.paneMode || !!state.splitPanes || state.activeSessionMode !== "gui";
|
|
247
|
+
button.classList.toggle("hidden", hidden);
|
|
248
|
+
if (hidden) closeMenu();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function initSessionActions() {
|
|
252
|
+
button = document.getElementById("header-session-actions-btn");
|
|
253
|
+
if (!button) return;
|
|
254
|
+
button.addEventListener("click", function () {
|
|
255
|
+
if (menu) closeMenu();
|
|
256
|
+
else showMainMenu();
|
|
257
|
+
});
|
|
258
|
+
store.subscribe(function (state, prev) {
|
|
259
|
+
if (state.activeSessionId !== prev.activeSessionId || state.dmMode !== prev.dmMode ||
|
|
260
|
+
state.paneMode !== prev.paneMode || state.splitPanes !== prev.splitPanes ||
|
|
261
|
+
state.activeSessionMode !== prev.activeSessionMode) updateVisibility();
|
|
262
|
+
});
|
|
263
|
+
window.addEventListener("resize", closeMenu);
|
|
264
|
+
updateVisibility();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function handleSessionActionMessage(msg) {
|
|
268
|
+
if (msg.type === "handoff_session_options") {
|
|
269
|
+
showHandoffDialog(msg);
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
if (msg.type === "session_handoff_result") {
|
|
273
|
+
if (msg.ok) closeHandoffDialog();
|
|
274
|
+
else {
|
|
275
|
+
showToast(msg.error || "Could not continue the session in another agent.", "error");
|
|
276
|
+
if (handoffSubmitButton) {
|
|
277
|
+
handoffSubmitButton.disabled = false;
|
|
278
|
+
handoffSubmitButton.textContent = "Continue";
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
if (msg.type === "handoff_context") {
|
|
284
|
+
var sourceName = VENDOR_NAMES[msg.sourceVendor] || msg.sourceVendor || "another agent";
|
|
285
|
+
addSystemMessage("Continued from “" + (msg.sourceTitle || "Untitled session") + "” in " + sourceName + ".", false);
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
if (msg.type === "handoff_created") {
|
|
289
|
+
var targetName = VENDOR_NAMES[msg.targetVendor] || msg.targetVendor || "another agent";
|
|
290
|
+
addSystemMessage("Continued in a new " + targetName + " session.", false);
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
resolveDefaultVendor,
|
|
16
16
|
startNewSession
|
|
17
17
|
} from './sidebar-sessions.js';
|
|
18
|
-
import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER, VENDOR_HOMEPAGES } from './app-rendering.js';
|
|
18
|
+
import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER, VENDOR_HOMEPAGES, isExperimentalVendor } from './app-rendering.js';
|
|
19
19
|
import {
|
|
20
20
|
getCachedProjectList,
|
|
21
21
|
getCachedCurrentSlug,
|
|
@@ -754,6 +754,7 @@ function renderMobileSessionsInto(container) {
|
|
|
754
754
|
if (vendor === mobileDefaultVendor) vBtn.classList.add("active");
|
|
755
755
|
vBtn.innerHTML = '<img src="' + VENDOR_AVATARS[vendor] + '" class="mobile-session-new-icon" alt="">' +
|
|
756
756
|
'<span>' + name + '</span>' +
|
|
757
|
+
(isExperimentalVendor(vendor) ? '<span class="vendor-experimental-badge" aria-label="Experimental integration" title="Experimental integration; not yet validated through direct use testing">🧪</span>' : '') +
|
|
757
758
|
(isInstalled ? "" : '<span class="mobile-vendor-note">Not installed</span>');
|
|
758
759
|
vBtn.addEventListener("click", function () {
|
|
759
760
|
if (!isInstalled) {
|
|
@@ -12,7 +12,7 @@ import { dismissOverlayPanels, closeSidebar, updatePageTitle, spawnDustParticles
|
|
|
12
12
|
import { showConfirm } from './app-misc.js';
|
|
13
13
|
import { getUpcomingSchedules } from './scheduler.js';
|
|
14
14
|
import { refreshMobileChatSheet } from './sidebar-mobile.js';
|
|
15
|
-
import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER, VENDOR_HOMEPAGES } from './app-rendering.js';
|
|
15
|
+
import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER, VENDOR_HOMEPAGES, isExperimentalVendor } from './app-rendering.js';
|
|
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';
|
|
@@ -394,10 +394,12 @@ function showNewSessionMenu(anchorBtn, defaultVendor) {
|
|
|
394
394
|
item.type = "button";
|
|
395
395
|
item.innerHTML = '<img src="' + VENDOR_AVATARS[vendor] + '" class="session-top-action-icon" alt="">' +
|
|
396
396
|
'<span class="session-new-vendor-name">' + name + '</span>' +
|
|
397
|
+
(isExperimentalVendor(vendor) ? '<span class="vendor-experimental-badge" aria-label="Experimental integration" title="Experimental integration; not yet validated through direct use testing">🧪</span>' : '') +
|
|
397
398
|
(isInstalled ? "" : '<span class="session-new-vendor-note">Not installed</span>' + iconHtml("external-link"));
|
|
398
399
|
item.title = isInstalled
|
|
399
400
|
? "New " + name + " session"
|
|
400
401
|
: name + " is not installed - open " + VENDOR_HOMEPAGES[vendor];
|
|
402
|
+
if (isExperimentalVendor(vendor)) item.title += " · Experimental integration; not yet validated through direct use testing";
|
|
401
403
|
item.addEventListener("click", function (e) {
|
|
402
404
|
e.stopPropagation();
|
|
403
405
|
closeSessionCtxMenu();
|
|
@@ -975,6 +977,9 @@ function renderLoopChild(s) {
|
|
|
975
977
|
if (s.isProcessing) {
|
|
976
978
|
textHtml += '<span class="session-processing"></span>';
|
|
977
979
|
}
|
|
980
|
+
if (s.backgroundTaskCount) {
|
|
981
|
+
textHtml += '<span class="session-background-task-count">' + s.backgroundTaskCount + '</span>';
|
|
982
|
+
}
|
|
978
983
|
if (s.loop) {
|
|
979
984
|
var isRalphChild = s.loop.source === "ralph";
|
|
980
985
|
var roleName = s.loop.role === "crafting" ? "Crafting" : s.loop.role === "judge" ? "Judge" : (isRalphChild ? "Coder" : "Run");
|
|
@@ -1094,6 +1099,9 @@ function renderLoopGroup(loopId, children, groupKey) {
|
|
|
1094
1099
|
if (anyProcessing) {
|
|
1095
1100
|
textHtml += '<span class="session-processing"></span>';
|
|
1096
1101
|
}
|
|
1102
|
+
var groupBackgroundTaskCount = 0;
|
|
1103
|
+
for (var bi = 0; bi < children.length; bi++) groupBackgroundTaskCount += children[bi].backgroundTaskCount || 0;
|
|
1104
|
+
if (groupBackgroundTaskCount) textHtml += '<span class="session-background-task-count">' + groupBackgroundTaskCount + '</span>';
|
|
1097
1105
|
var groupIcon = isDebate ? "mic" : (isRalph ? "repeat" : "calendar-clock");
|
|
1098
1106
|
var iconClass = isDebate ? " debate" : (isRalph ? "" : " scheduled");
|
|
1099
1107
|
textHtml += '<span class="session-loop-icon' + iconClass + '">' + iconHtml(groupIcon) + '</span>';
|
|
@@ -1203,6 +1211,9 @@ function renderLoopRun(parentGk, startedAtKey, sessions, isRalph) {
|
|
|
1203
1211
|
if (anyProcessing) {
|
|
1204
1212
|
textHtml += '<span class="session-processing"></span>';
|
|
1205
1213
|
}
|
|
1214
|
+
var runBackgroundTaskCount = 0;
|
|
1215
|
+
for (var bi = 0; bi < sessions.length; bi++) runBackgroundTaskCount += sessions[bi].backgroundTaskCount || 0;
|
|
1216
|
+
if (runBackgroundTaskCount) textHtml += '<span class="session-background-task-count">' + runBackgroundTaskCount + '</span>';
|
|
1206
1217
|
textHtml += '<span class="session-loop-run-time">' + escapeHtml(timeLabel) + '</span>';
|
|
1207
1218
|
textHtml += '<span class="session-loop-count' + (isRalph ? "" : " scheduled") + '">' + sessions.length + '</span>';
|
|
1208
1219
|
textSpan.innerHTML = textHtml;
|
|
@@ -1252,6 +1263,9 @@ function renderSessionItem(s) {
|
|
|
1252
1263
|
if (s.isProcessing) {
|
|
1253
1264
|
textHtml += '<span class="session-processing"></span>';
|
|
1254
1265
|
}
|
|
1266
|
+
if (s.backgroundTaskCount) {
|
|
1267
|
+
textHtml += '<span class="session-background-task-count">' + s.backgroundTaskCount + '</span>';
|
|
1268
|
+
}
|
|
1255
1269
|
if (s.loop && s.loop.source === "debate") {
|
|
1256
1270
|
textHtml += '<span class="session-debate-icon" title="Debate">' + iconHtml("mic") + '</span>';
|
|
1257
1271
|
}
|
|
@@ -1320,6 +1334,8 @@ function renderSplitGroupItem(group, members) {
|
|
|
1320
1334
|
}
|
|
1321
1335
|
html += "</span>";
|
|
1322
1336
|
if (members[0].isProcessing || members[1].isProcessing) html += '<span class="session-processing"></span>';
|
|
1337
|
+
var splitBackgroundTaskCount = (members[0].backgroundTaskCount || 0) + (members[1].backgroundTaskCount || 0);
|
|
1338
|
+
if (splitBackgroundTaskCount) html += '<span class="session-background-task-count">' + splitBackgroundTaskCount + '</span>';
|
|
1323
1339
|
html += '<span class="split-group-name">' + highlightMatch(group.name || "Split", searchQuery) + "</span>";
|
|
1324
1340
|
text.innerHTML = html;
|
|
1325
1341
|
el.appendChild(text);
|