clay-server 5.0.0-beta.2 → 5.0.0-beta.4
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 +9 -0
- package/lib/new-session-default.js +45 -0
- package/lib/project-connection.js +4 -2
- package/lib/project-session-creation.js +107 -0
- package/lib/project-session-delete.js +21 -3
- package/lib/project-session-folder-delete.js +169 -0
- package/lib/project-session-folders.js +272 -0
- package/lib/project-sessions.js +46 -26
- package/lib/project.js +2 -0
- package/lib/public/app.js +12 -0
- package/lib/public/css/filebrowser.css +3 -3
- package/lib/public/css/fonts.css +6 -0
- package/lib/public/css/home-hub.css +52 -0
- package/lib/public/css/mates.css +1 -1
- package/lib/public/css/mobile-nav.css +0 -128
- package/lib/public/css/session-folders.css +402 -0
- package/lib/public/css/sidebar.css +28 -545
- package/lib/public/index.html +13 -22
- package/lib/public/modules/app-favicon.js +5 -4
- package/lib/public/modules/app-home-hub.js +60 -7
- package/lib/public/modules/app-messages.js +13 -0
- package/lib/public/modules/home-project-empty-access.js +44 -0
- package/lib/public/modules/input.js +3 -0
- package/lib/public/modules/project-activation.js +11 -0
- package/lib/public/modules/project-switcher.js +4 -4
- package/lib/public/modules/session-create-form.js +428 -0
- package/lib/public/modules/session-create.js +340 -0
- package/lib/public/modules/session-date-group-header.js +57 -0
- package/lib/public/modules/session-date-groups.js +84 -0
- package/lib/public/modules/session-folder-collapse.js +192 -0
- package/lib/public/modules/session-folder-context.js +215 -0
- package/lib/public/modules/session-folder-delete.js +339 -0
- package/lib/public/modules/session-folder-dialogs.js +173 -0
- package/lib/public/modules/session-folder-dnd.js +214 -0
- package/lib/public/modules/session-folder-draft.js +184 -0
- package/lib/public/modules/session-folder-layout.js +133 -0
- package/lib/public/modules/session-folder-toolbar.js +185 -0
- package/lib/public/modules/session-folder-view-menu.js +187 -0
- package/lib/public/modules/session-folders.js +97 -0
- package/lib/public/modules/session-list-search.js +108 -0
- package/lib/public/modules/session-presence-ui.js +44 -0
- package/lib/public/modules/sidebar-creation-effect.js +130 -0
- package/lib/public/modules/sidebar-mobile.js +39 -182
- package/lib/public/modules/sidebar-session-folders-ui.js +394 -0
- package/lib/public/modules/sidebar-sessions.js +142 -718
- package/lib/public/modules/sidebar.js +30 -7
- package/lib/public/modules/terminal.js +2 -2
- package/lib/public/modules/tool-palette.js +37 -219
- package/lib/public/style.css +1 -0
- package/lib/server.js +4 -0
- package/lib/session-folders.js +414 -0
- package/lib/sessions.js +7 -5
- package/lib/users-session-folder-preferences.js +69 -0
- package/lib/users.js +6 -0
- package/lib/ws-schema.js +11 -0
- package/package.json +1 -1
- package/lib/public/modules/session-creation-cta.js +0 -46
- package/lib/public/modules/tool-palette-overlays.js +0 -159
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
// The inline "new session" draft row that sits under a folder header (also when
|
|
2
|
+
// the folder is collapsed): provider icon, a compact provider picker and icon
|
|
3
|
+
// Cancel. The stable header action becomes Create. State and server round
|
|
4
|
+
// trips are in session-create.js; this module only builds and repaints the DOM
|
|
5
|
+
// from the store, so the row keeps its provider, focus and error across list
|
|
6
|
+
// rerenders and server updates.
|
|
7
|
+
|
|
8
|
+
import { store } from './store.js';
|
|
9
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
10
|
+
import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER, isExperimentalVendor } from './app-rendering.js';
|
|
11
|
+
import { startNewSession } from './sidebar-sessions.js';
|
|
12
|
+
import { closeMobileSheet } from './sidebar-mobile.js';
|
|
13
|
+
import {
|
|
14
|
+
createState, createOptions, installedVendorIds,
|
|
15
|
+
openSessionCreate, closeSessionCreate, chooseVendor, setCreateMenu, submitSessionCreate, saveProjectDefault, retryOptions
|
|
16
|
+
} from './session-create.js';
|
|
17
|
+
|
|
18
|
+
// Every registered provider, in the established order, followed by any the
|
|
19
|
+
// server reports that this client does not know. Installed state is the
|
|
20
|
+
// server's, per user; the dropdown never invents or hardcodes availability.
|
|
21
|
+
function vendorEntries(options) {
|
|
22
|
+
var byId = {};
|
|
23
|
+
options.vendors.forEach(function (v) { byId[v.id] = v; });
|
|
24
|
+
var ids = VENDOR_ORDER.slice();
|
|
25
|
+
options.vendors.forEach(function (v) { if (ids.indexOf(v.id) === -1) ids.push(v.id); });
|
|
26
|
+
return ids.map(function (id) {
|
|
27
|
+
var known = byId[id];
|
|
28
|
+
return { id: id, name: VENDOR_NAMES[id] || (known && known.displayName) || id, installed: !!(known && known.installed) };
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function vendorLabel(id) {
|
|
33
|
+
return VENDOR_NAMES[id] || id;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Enter on a select confirms its native option list and on a button activates
|
|
37
|
+
// that button; only other targets submit the row.
|
|
38
|
+
function ownsEnter(target) {
|
|
39
|
+
var tag = target && target.tagName;
|
|
40
|
+
return tag === "SELECT" || tag === "BUTTON" || tag === "A" || tag === "TEXTAREA" || tag === "OPTION";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function iconButton(icon, label, key, onClick) {
|
|
44
|
+
var btn = document.createElement("button");
|
|
45
|
+
btn.type = "button";
|
|
46
|
+
btn.className = "session-create-icon-btn";
|
|
47
|
+
btn.dataset.focusKey = key;
|
|
48
|
+
btn.innerHTML = iconHtml(icon);
|
|
49
|
+
btn.setAttribute("aria-label", label);
|
|
50
|
+
btn.title = label;
|
|
51
|
+
btn.addEventListener("click", onClick);
|
|
52
|
+
return btn;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function focusHeaderButton(folderId) {
|
|
56
|
+
function applyFocus() {
|
|
57
|
+
var buttons = document.querySelectorAll('[data-new-session-button="' + folderId + '"]');
|
|
58
|
+
for (var i = 0; i < buttons.length; i++) if (buttons[i].getClientRects().length) { buttons[i].focus({ preventScroll: true }); return; }
|
|
59
|
+
}
|
|
60
|
+
queueMicrotask(applyFocus);
|
|
61
|
+
requestAnimationFrame(applyFocus);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function prefersReducedMotion() {
|
|
65
|
+
return typeof window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function ensureNewSessionButtonContent(btn) {
|
|
69
|
+
if (btn.querySelector(".session-folder-new-label-text")) return;
|
|
70
|
+
btn.innerHTML = '<span class="session-folder-new-icon" aria-hidden="true">' + iconHtml("plus") + '</span>' +
|
|
71
|
+
'<span class="session-folder-new-label"><span class="session-folder-new-label-clip">' +
|
|
72
|
+
'<span class="session-folder-new-label-previous" aria-hidden="true"></span>' +
|
|
73
|
+
'<span class="session-folder-new-label-text">New session</span>' +
|
|
74
|
+
'<span class="session-folder-new-label-sizer" aria-hidden="true">Click to create</span>' +
|
|
75
|
+
'</span></span>';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function setNewSessionButtonText(btn, text, animate) {
|
|
79
|
+
var current = btn.querySelector(".session-folder-new-label-text");
|
|
80
|
+
var previous = btn.querySelector(".session-folder-new-label-previous");
|
|
81
|
+
if (!current || current.textContent === text) return;
|
|
82
|
+
var revision = String((parseInt(btn.dataset.labelRevision || "0", 10) || 0) + 1);
|
|
83
|
+
btn.dataset.labelRevision = revision;
|
|
84
|
+
previous.textContent = current.textContent;
|
|
85
|
+
current.textContent = text;
|
|
86
|
+
btn.classList.remove("is-label-changing");
|
|
87
|
+
if (!animate || prefersReducedMotion()) { previous.textContent = ""; return; }
|
|
88
|
+
void btn.offsetWidth;
|
|
89
|
+
btn.classList.add("is-label-changing");
|
|
90
|
+
setTimeout(function () {
|
|
91
|
+
if (btn.dataset.labelRevision !== revision) return;
|
|
92
|
+
btn.classList.remove("is-label-changing");
|
|
93
|
+
previous.textContent = "";
|
|
94
|
+
}, 190);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function createStatusMessage(form, options, valid) {
|
|
98
|
+
if (form.error) return form.error;
|
|
99
|
+
if (options.error) return options.error;
|
|
100
|
+
if (form.phase === "pending") return "Creating session…";
|
|
101
|
+
if (!options.loaded) return "Loading providers…";
|
|
102
|
+
if (!valid) return "Choose an installed provider first.";
|
|
103
|
+
return form.notice || "";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function paintNewSessionButton(btn, folderId, label) {
|
|
107
|
+
var form = createState();
|
|
108
|
+
var active = !!(form && form.folderId === folderId);
|
|
109
|
+
var options = createOptions();
|
|
110
|
+
var valid = active && options.loaded && installedVendorIds().indexOf(form.vendor) !== -1;
|
|
111
|
+
var pending = active && form.phase === "pending";
|
|
112
|
+
var buttonText = !active ? "New session" : (pending ? "Creating…" : (!options.loaded ? "Loading…" : (!valid ? "Choose provider" : "Click to create")));
|
|
113
|
+
var status = active ? createStatusMessage(form, options, valid) : "";
|
|
114
|
+
ensureNewSessionButtonContent(btn);
|
|
115
|
+
btn.classList.toggle("is-create", active);
|
|
116
|
+
btn.setAttribute("aria-expanded", String(active));
|
|
117
|
+
btn.setAttribute("aria-busy", String(pending));
|
|
118
|
+
btn.setAttribute("aria-label", active ? (pending ? "Creating session in " : (valid ? "Click to create session in " : buttonText + " for ")) + label : "New session in " + label);
|
|
119
|
+
var controls = btn.getAttribute("aria-controls");
|
|
120
|
+
if (active && status && controls) btn.setAttribute("aria-describedby", controls + "-status");
|
|
121
|
+
else btn.removeAttribute("aria-describedby");
|
|
122
|
+
btn.disabled = active ? (pending || !valid) : !!(form && form.phase === "pending");
|
|
123
|
+
setNewSessionButtonText(btn, buttonText, active && (!form.openedAt || Date.now() - form.openedAt < 400 || btn.isConnected));
|
|
124
|
+
refreshIcons();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// The quiet "+ New session" pill the folder header places on its right.
|
|
128
|
+
export function renderNewSessionButton(folderId, label, surface) {
|
|
129
|
+
var btn = document.createElement("button");
|
|
130
|
+
btn.type = "button";
|
|
131
|
+
btn.className = "session-folder-new-btn";
|
|
132
|
+
btn.dataset.newSessionButton = folderId;
|
|
133
|
+
btn.setAttribute("aria-controls", "session-create-row-" + surface + "-" + folderId);
|
|
134
|
+
btn.dataset.folderLabel = label;
|
|
135
|
+
paintNewSessionButton(btn, folderId, label);
|
|
136
|
+
btn.addEventListener("click", function (event) {
|
|
137
|
+
event.stopPropagation();
|
|
138
|
+
var live = createState();
|
|
139
|
+
if (live && live.folderId === folderId) { submitAndClose(); return; }
|
|
140
|
+
openSessionCreate(folderId);
|
|
141
|
+
focusHeaderButton(folderId);
|
|
142
|
+
});
|
|
143
|
+
return btn;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function closeAndFocus(folderId) {
|
|
147
|
+
closeSessionCreate();
|
|
148
|
+
queueMicrotask(function () {
|
|
149
|
+
var buttons = document.querySelectorAll('[data-new-session-button="' + folderId + '"]');
|
|
150
|
+
for (var i = 0; i < buttons.length; i++) if (buttons[i].getClientRects().length) { buttons[i].focus(); return; }
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function renderCreateRow(folderId, label, surface) {
|
|
155
|
+
var form = createState();
|
|
156
|
+
if (!form || form.folderId !== folderId) return null;
|
|
157
|
+
var root = document.createElement("div");
|
|
158
|
+
root.className = "session-create-row" + (surface === "mobile" ? " is-mobile" : "");
|
|
159
|
+
root.id = "session-create-row-" + surface + "-" + folderId;
|
|
160
|
+
root.setAttribute("role", "group");
|
|
161
|
+
root.setAttribute("aria-label", "New session in " + label);
|
|
162
|
+
root.dataset.folderId = folderId;
|
|
163
|
+
root.addEventListener("keydown", function (event) {
|
|
164
|
+
// Enter and Escape belong to the input method while composing.
|
|
165
|
+
if (event.isComposing || event.keyCode === 229) return;
|
|
166
|
+
var live = createState();
|
|
167
|
+
if (!live) return;
|
|
168
|
+
if (live.menuOpen && onMenuKey(event, live)) return;
|
|
169
|
+
if (event.key === "Escape") {
|
|
170
|
+
event.preventDefault();
|
|
171
|
+
event.stopPropagation();
|
|
172
|
+
if (live.phase !== "pending") closeAndFocus(folderId);
|
|
173
|
+
} else if (event.key === "Enter" && !ownsEnter(event.target)) {
|
|
174
|
+
event.preventDefault();
|
|
175
|
+
submitAndClose();
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
paintRow(root);
|
|
179
|
+
// Once attached: restore focus (the first paint ran detached) and scroll the
|
|
180
|
+
// row into view a single time when it was just opened.
|
|
181
|
+
queueMicrotask(function () {
|
|
182
|
+
var live = createState();
|
|
183
|
+
if (!live || !root.isConnected || !root.getClientRects().length) return;
|
|
184
|
+
if (live.wantFocus) paintRow(root);
|
|
185
|
+
if (live.restoreFocusKey) {
|
|
186
|
+
var again = root.querySelector('[data-focus-key="' + live.restoreFocusKey + '"]');
|
|
187
|
+
if (again && !again.disabled) again.focus({ preventScroll: true });
|
|
188
|
+
store.set({ sessionCreate: Object.assign({}, createState(), { restoreFocusKey: null }) });
|
|
189
|
+
}
|
|
190
|
+
if (live.scroll) {
|
|
191
|
+
if (root.scrollIntoView) root.scrollIntoView({ block: "nearest" });
|
|
192
|
+
store.set({ sessionCreate: Object.assign({}, createState(), { scroll: false }) });
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
return root;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function closeMenu(refocus) {
|
|
199
|
+
var live = createState();
|
|
200
|
+
if (!live || !live.menuOpen) return;
|
|
201
|
+
var change = { menuOpen: false, menuFocus: false };
|
|
202
|
+
if (refocus) { change.focusKey = "vendor"; change.wantFocus = true; }
|
|
203
|
+
store.set({ sessionCreate: Object.assign({}, live, change) });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function onMenuKey(event, live) {
|
|
207
|
+
var items = Array.prototype.slice.call(document.querySelectorAll('.session-create-menu [role="menuitem"]:not(:disabled)'));
|
|
208
|
+
var at = items.indexOf(document.activeElement);
|
|
209
|
+
var move = function (to) { event.preventDefault(); event.stopPropagation(); if (items[to]) items[to].focus(); };
|
|
210
|
+
if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); closeMenu(true); return true; }
|
|
211
|
+
if (event.key === "ArrowDown") { move((at + 1) % items.length); return true; }
|
|
212
|
+
if (event.key === "ArrowUp") { move((at - 1 + items.length) % items.length); return true; }
|
|
213
|
+
if (event.key === "Home") { move(0); return true; }
|
|
214
|
+
if (event.key === "End") { move(items.length - 1); return true; }
|
|
215
|
+
if (event.key === "Tab") { closeMenu(false); return true; }
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function submitAndClose() {
|
|
220
|
+
submitSessionCreate(startNewSession);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function paintRow(root) {
|
|
224
|
+
var form = createState();
|
|
225
|
+
if (!form) return;
|
|
226
|
+
var options = createOptions();
|
|
227
|
+
var pending = form.phase === "pending";
|
|
228
|
+
var active = document.activeElement;
|
|
229
|
+
var focusKey = active && root.contains(active) ? active.dataset.focusKey : null;
|
|
230
|
+
// An explicit focus request wins over wherever focus happens to be.
|
|
231
|
+
if (form.wantFocus && root.getClientRects().length) focusKey = form.focusKey || "vendor";
|
|
232
|
+
root.setAttribute("aria-busy", String(pending));
|
|
233
|
+
root.innerHTML = "";
|
|
234
|
+
|
|
235
|
+
var line = document.createElement("div");
|
|
236
|
+
line.className = "session-create-line";
|
|
237
|
+
var installed = installedVendorIds();
|
|
238
|
+
|
|
239
|
+
var icon = document.createElement("img");
|
|
240
|
+
icon.className = "session-create-icon";
|
|
241
|
+
icon.alt = "";
|
|
242
|
+
icon.src = VENDOR_AVATARS[form.vendor] || VENDOR_AVATARS.claude;
|
|
243
|
+
line.appendChild(icon);
|
|
244
|
+
|
|
245
|
+
var entries = vendorEntries(options);
|
|
246
|
+
var picker = document.createElement("button");
|
|
247
|
+
picker.type = "button";
|
|
248
|
+
picker.className = "session-create-picker";
|
|
249
|
+
picker.dataset.focusKey = "vendor";
|
|
250
|
+
picker.setAttribute("aria-haspopup", "menu");
|
|
251
|
+
picker.setAttribute("aria-expanded", String(!!form.menuOpen));
|
|
252
|
+
picker.setAttribute("aria-label", "Provider for the new session: " + (installed.indexOf(form.vendor) === -1 ? "none chosen" : vendorLabel(form.vendor)));
|
|
253
|
+
picker.disabled = pending || !options.loaded;
|
|
254
|
+
var pickerText = !options.loaded ? (options.error ? "Providers unavailable" : "Loading providers") : (installed.indexOf(form.vendor) === -1 ? (installed.length ? "Choose a provider" : "No provider installed") : vendorLabel(form.vendor));
|
|
255
|
+
picker.innerHTML = '<span class="session-create-picker-name"></span>' + iconHtml("chevron-down");
|
|
256
|
+
picker.firstChild.textContent = pickerText;
|
|
257
|
+
picker.addEventListener("click", function () {
|
|
258
|
+
var live = createState();
|
|
259
|
+
if (live.menuOpen) closeMenu(true);
|
|
260
|
+
else setCreateMenu(true, Math.max(0, entries.map(function (e) { return e.id; }).indexOf(live.vendor)));
|
|
261
|
+
});
|
|
262
|
+
picker.addEventListener("keydown", function (event) {
|
|
263
|
+
if ((event.key === "ArrowDown" || event.key === "ArrowUp") && !event.isComposing && !picker.disabled) {
|
|
264
|
+
event.preventDefault();
|
|
265
|
+
event.stopPropagation();
|
|
266
|
+
if (!createState().menuOpen) setCreateMenu(true, Math.max(0, entries.map(function (e) { return e.id; }).indexOf(createState().vendor)));
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
line.appendChild(picker);
|
|
270
|
+
|
|
271
|
+
var cancel = iconButton("x", "Cancel new session", "cancel", function () { closeAndFocus(form.folderId); });
|
|
272
|
+
cancel.disabled = pending;
|
|
273
|
+
line.appendChild(cancel);
|
|
274
|
+
root.appendChild(line);
|
|
275
|
+
|
|
276
|
+
var valid = options.loaded && installed.indexOf(form.vendor) !== -1;
|
|
277
|
+
var optionsError = options.error || "";
|
|
278
|
+
var message = createStatusMessage(form, options, valid);
|
|
279
|
+
var status = document.createElement("div");
|
|
280
|
+
status.id = root.id + "-status";
|
|
281
|
+
status.className = "session-create-status" + (form.error || optionsError ? " is-error" : "");
|
|
282
|
+
status.setAttribute("role", form.error || optionsError ? "alert" : "status");
|
|
283
|
+
status.textContent = message;
|
|
284
|
+
if (!options.loaded && options.error) {
|
|
285
|
+
var retry = document.createElement("button");
|
|
286
|
+
retry.type = "button";
|
|
287
|
+
retry.className = "session-create-retry";
|
|
288
|
+
retry.dataset.focusKey = "retry";
|
|
289
|
+
retry.textContent = "Retry";
|
|
290
|
+
retry.addEventListener("click", function () { retryOptions(); });
|
|
291
|
+
status.appendChild(document.createTextNode(" "));
|
|
292
|
+
status.appendChild(retry);
|
|
293
|
+
}
|
|
294
|
+
root.appendChild(status);
|
|
295
|
+
if (form.menuOpen && !pending) root.appendChild(renderMenu(entries, form, options, installed));
|
|
296
|
+
refreshIcons();
|
|
297
|
+
|
|
298
|
+
if (focusKey) {
|
|
299
|
+
var target = root.querySelector('[data-focus-key="' + focusKey + '"]');
|
|
300
|
+
var focused = false;
|
|
301
|
+
if (target && !target.disabled) { target.focus({ preventScroll: true }); focused = true; }
|
|
302
|
+
else if (target && active && root.contains(active)) {
|
|
303
|
+
var fallback = root.querySelector('[data-focus-key="cancel"]');
|
|
304
|
+
if (fallback && !fallback.disabled) { fallback.focus({ preventScroll: true }); focused = true; }
|
|
305
|
+
}
|
|
306
|
+
// A requested focus is consumed only once a control could take it, so a row
|
|
307
|
+
// still loading its providers keeps asking.
|
|
308
|
+
if (focused && form.wantFocus) store.set({ sessionCreate: Object.assign({}, createState(), { wantFocus: false }) });
|
|
309
|
+
}
|
|
310
|
+
placeMenu(root);
|
|
311
|
+
if (form.menuOpen && form.menuFocus && root.getClientRects().length) {
|
|
312
|
+
var first = root.querySelector('[data-focus-key="item:' + (form.menuIndex || 0) + '"]');
|
|
313
|
+
if (first) { first.focus({ preventScroll: true }); store.set({ sessionCreate: Object.assign({}, createState(), { menuFocus: false }) }); }
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function menuItem(key, className, html, onClick, label) {
|
|
318
|
+
var item = document.createElement("button");
|
|
319
|
+
item.type = "button";
|
|
320
|
+
item.className = "session-create-menu-item " + className;
|
|
321
|
+
item.setAttribute("role", "menuitem");
|
|
322
|
+
item.dataset.focusKey = key;
|
|
323
|
+
item.innerHTML = html;
|
|
324
|
+
if (label) item.title = label;
|
|
325
|
+
item.addEventListener("click", function (event) { event.stopPropagation(); onClick(); });
|
|
326
|
+
return item;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// The compact provider menu keeps every registered provider visible. Installed
|
|
330
|
+
// entries are selectable; unavailable entries are plain disabled rows. One secondary item sets the project
|
|
331
|
+
// default action beside each available provider (permission-gated, provider only).
|
|
332
|
+
function renderMenu(entries, form, options, installed) {
|
|
333
|
+
var menu = document.createElement("div");
|
|
334
|
+
menu.className = "session-create-menu";
|
|
335
|
+
menu.setAttribute("role", "menu");
|
|
336
|
+
menu.setAttribute("aria-label", "Providers");
|
|
337
|
+
entries.forEach(function (entry, index) {
|
|
338
|
+
var row = document.createElement("div");
|
|
339
|
+
row.className = "session-create-menu-provider";
|
|
340
|
+
var html = '<img src="' + (VENDOR_AVATARS[entry.id] || VENDOR_AVATARS.claude) + '" class="session-create-icon" alt="">' +
|
|
341
|
+
'<span class="session-create-menu-name"></span>' +
|
|
342
|
+
(entry.installed && isExperimentalVendor(entry.id) ? '<span class="vendor-experimental-badge" aria-label="Experimental integration" title="Experimental integration; not yet validated through direct use testing">🧪</span>' : "") +
|
|
343
|
+
(entry.installed ? (options.projectDefault === entry.id ? '<span class="session-create-menu-note">Default</span>' : "") : '<span class="session-create-menu-note">Not installed</span>');
|
|
344
|
+
var item = menuItem("item:" + index, (entry.installed ? "" : "unavailable ") + (entry.id === form.vendor ? "selected" : ""), html, function () {
|
|
345
|
+
if (entry.installed) { chooseVendor(entry.id); var live = createState(); store.set({ sessionCreate: Object.assign({}, live, { focusKey: "vendor", wantFocus: true }) }); }
|
|
346
|
+
}, entry.installed ? "New " + entry.name + " session" : entry.name + " is not installed");
|
|
347
|
+
item.querySelector(".session-create-menu-name").textContent = entry.name;
|
|
348
|
+
item.disabled = !entry.installed;
|
|
349
|
+
item.setAttribute("aria-checked", String(entry.id === form.vendor));
|
|
350
|
+
if (options.projectDefault === entry.id) item.setAttribute("aria-current", "true");
|
|
351
|
+
row.appendChild(item);
|
|
352
|
+
if (entry.installed && options.canSetProjectDefault && options.projectDefault !== entry.id) {
|
|
353
|
+
var savingThis = form.defaultSaving && form.defaultVendor === entry.id;
|
|
354
|
+
var setDefault = menuItem("default:" + index, "session-create-default-btn", "<span></span>", function () { saveProjectDefault(entry.id); }, "Set " + entry.name + " as the project default");
|
|
355
|
+
setDefault.firstChild.textContent = savingThis ? "Saving" : "Set default";
|
|
356
|
+
setDefault.disabled = !!form.defaultSaving;
|
|
357
|
+
row.appendChild(setDefault);
|
|
358
|
+
}
|
|
359
|
+
menu.appendChild(row);
|
|
360
|
+
});
|
|
361
|
+
return menu;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Anchors the open menu under the picker and keeps it inside the viewport.
|
|
365
|
+
function placeMenu(root) {
|
|
366
|
+
var menu = root.querySelector(".session-create-menu");
|
|
367
|
+
var picker = root.querySelector(".session-create-picker");
|
|
368
|
+
if (!menu || !picker || !root.getClientRects().length) return;
|
|
369
|
+
var r = picker.getBoundingClientRect();
|
|
370
|
+
var width = Math.min(root.classList.contains("is-mobile") ? 320 : 276, window.innerWidth - 16);
|
|
371
|
+
menu.style.width = width + "px";
|
|
372
|
+
menu.style.left = Math.min(Math.max(8, r.left), window.innerWidth - width - 8) + "px";
|
|
373
|
+
var height = menu.getBoundingClientRect().height;
|
|
374
|
+
var top = r.bottom + 4;
|
|
375
|
+
if (top + height > window.innerHeight - 8) top = Math.max(8, r.top - height - 4);
|
|
376
|
+
menu.style.top = top + "px";
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Repaints every visible copy in place (desktop and mobile) without rebuilding
|
|
380
|
+
// the whole session list.
|
|
381
|
+
function repaintAll() {
|
|
382
|
+
var roots = document.querySelectorAll(".session-create-row");
|
|
383
|
+
for (var i = 0; i < roots.length; i++) paintRow(roots[i]);
|
|
384
|
+
var buttons = document.querySelectorAll(".session-folder-new-btn");
|
|
385
|
+
for (var b = 0; b < buttons.length; b++) paintNewSessionButton(buttons[b], buttons[b].dataset.newSessionButton, buttons[b].dataset.folderLabel || "folder");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Called right before a list rerender discards the row, so the rebuilt one can
|
|
389
|
+
// put focus back where the user was.
|
|
390
|
+
export function captureCreateFocus() {
|
|
391
|
+
var live = createState();
|
|
392
|
+
if (!live) return;
|
|
393
|
+
var roots = document.querySelectorAll(".session-create-row");
|
|
394
|
+
var focusKey = null;
|
|
395
|
+
for (var i = 0; i < roots.length; i++) {
|
|
396
|
+
if (roots[i].contains(document.activeElement) && document.activeElement.dataset.focusKey) focusKey = document.activeElement.dataset.focusKey;
|
|
397
|
+
}
|
|
398
|
+
if ((live.restoreFocusKey || null) !== focusKey) store.set({ sessionCreate: Object.assign({}, live, { restoreFocusKey: focusKey }) });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
store.subscribe(function (state, previous) {
|
|
402
|
+
var a = state.sessionCreate;
|
|
403
|
+
var b = previous.sessionCreate;
|
|
404
|
+
// A creation acknowledged from the phone sheet closes it, like the old flow.
|
|
405
|
+
if (b && b.phase === "pending" && !a && state.sessionCreateReveal && state.sessionCreateReveal.sessionId) closeMobileSheet();
|
|
406
|
+
var structural = !a !== !b || (a && b && a.folderId !== b.folderId);
|
|
407
|
+
if (structural) return;
|
|
408
|
+
if (a !== b || state.sessionCreateOptions !== previous.sessionCreateOptions) repaintAll();
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// A press outside the open provider menu closes it without cancelling the press
|
|
412
|
+
// or taking focus; pointerdown is primary because some surfaces cancel it.
|
|
413
|
+
function onOutsidePress(event) {
|
|
414
|
+
var live = createState();
|
|
415
|
+
if (!live || !live.menuOpen) return;
|
|
416
|
+
var target = event.target;
|
|
417
|
+
if (target && target.nodeType === 1 && target.closest && (target.closest(".session-create-menu") || target.closest(".session-create-picker"))) return;
|
|
418
|
+
closeMenu(false);
|
|
419
|
+
}
|
|
420
|
+
// Test stubs that import this module without a full DOM have no listeners to add.
|
|
421
|
+
if (typeof document !== "undefined" && typeof document.addEventListener === "function") {
|
|
422
|
+
document.addEventListener("pointerdown", onOutsidePress, true);
|
|
423
|
+
document.addEventListener("mousedown", onOutsidePress, true);
|
|
424
|
+
document.addEventListener("touchstart", onOutsidePress, true);
|
|
425
|
+
}
|
|
426
|
+
if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
|
|
427
|
+
window.addEventListener("blur", function () { closeMenu(false); });
|
|
428
|
+
}
|