clay-server 3.4.0-beta.8 → 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-projects.js +3 -3
- package/lib/daemon.js +59 -59
- 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 +35 -79
- package/lib/public/app.js +11 -0
- package/lib/public/copilot-avatar.svg +5 -0
- package/lib/public/css/command-palette.css +22 -2
- package/lib/public/css/filebrowser.css +6 -1
- package/lib/public/css/icon-strip.css +29 -13
- 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 +3 -1
- 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/project-switcher.js +7 -6
- package/lib/public/modules/session-actions.js +294 -0
- package/lib/public/modules/sidebar-mobile.js +2 -1
- package/lib/public/modules/sidebar-projects.js +34 -43
- 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/modules/worktree-location.js +17 -0
- 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/worktree.js +9 -4
- 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() {
|
|
@@ -19,6 +19,7 @@ import { getCachedProjects, switchProject } from './app-projects.js';
|
|
|
19
19
|
import { openDm } from './app-dm.js';
|
|
20
20
|
import { mateAvatarUrl } from './avatar.js';
|
|
21
21
|
import { parseEmojis } from './markdown.js';
|
|
22
|
+
import { isExternalWorktree, externalWorktreeTooltip, appendExternalWorktreeBadge } from './worktree-location.js';
|
|
22
23
|
|
|
23
24
|
var _projectMru = []; // project slugs, most recent first
|
|
24
25
|
var _mateMru = []; // mate ids (mate_XXX), most recent first
|
|
@@ -257,18 +258,16 @@ function buildProjectEntries() {
|
|
|
257
258
|
// Keep natural (server-provided) order so the list looks the same
|
|
258
259
|
// every time the switcher opens. MRU only drives initial highlight.
|
|
259
260
|
return projects.map(function (p) {
|
|
260
|
-
|
|
261
|
-
// unmounted or moved); those are effectively unreachable from
|
|
262
|
-
// this session and shouldn't be switchable.
|
|
263
|
-
var unreachable = p.isWorktree && p.worktreeAccessible === false;
|
|
261
|
+
var external = isExternalWorktree(p);
|
|
264
262
|
return {
|
|
265
263
|
key: p.slug,
|
|
266
264
|
title: p.title || p.project || p.slug,
|
|
267
265
|
icon: p.icon || null,
|
|
268
266
|
fallbackIcon: 'folder',
|
|
269
267
|
isCurrent: p.slug === store.get('currentSlug'),
|
|
270
|
-
|
|
271
|
-
|
|
268
|
+
external: external,
|
|
269
|
+
disabled: false,
|
|
270
|
+
disabledReason: null,
|
|
272
271
|
};
|
|
273
272
|
});
|
|
274
273
|
}
|
|
@@ -336,6 +335,7 @@ function buildEntryNode(entry, index) {
|
|
|
336
335
|
item.className = 'cmd-palette-item project-switcher-item';
|
|
337
336
|
if (entry.disabled) item.classList.add('disabled');
|
|
338
337
|
if (entry.disabled && entry.disabledReason) item.title = entry.disabledReason;
|
|
338
|
+
if (entry.external) item.title = externalWorktreeTooltip(entry.title);
|
|
339
339
|
item.dataset.index = String(index);
|
|
340
340
|
|
|
341
341
|
var iconWrap = document.createElement('div');
|
|
@@ -353,6 +353,7 @@ function buildEntryNode(entry, index) {
|
|
|
353
353
|
fallback.setAttribute('data-lucide', entry.fallbackIcon || 'folder');
|
|
354
354
|
iconWrap.appendChild(fallback);
|
|
355
355
|
}
|
|
356
|
+
if (entry.external) appendExternalWorktreeBadge(iconWrap, 'project-switcher-external-badge');
|
|
356
357
|
item.appendChild(iconWrap);
|
|
357
358
|
|
|
358
359
|
var body = document.createElement('div');
|
|
@@ -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) {
|