clay-server 4.8.0-beta.1 → 4.8.0-beta.3
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/bundled-skills/clay-agent-browser/SKILL.md +36 -0
- package/lib/clay-skill-install.js +33 -0
- package/lib/clay-skill-storage.js +49 -0
- package/lib/linear-api.js +62 -0
- package/lib/linear-store.js +52 -0
- package/lib/project-http.js +14 -5
- package/lib/project-linear-launch.js +70 -0
- package/lib/project-session-linear.js +197 -0
- package/lib/project-shared-browser.js +256 -0
- package/lib/project.js +74 -2
- package/lib/public/app.js +7 -0
- package/lib/public/css/filebrowser.css +2 -25
- package/lib/public/css/issues.css +1 -14
- package/lib/public/css/linear.css +35 -0
- package/lib/public/css/mcp-skills-workbench.css +51 -0
- package/lib/public/css/project-logs.css +0 -11
- package/lib/public/css/right-workbench.css +28 -0
- package/lib/public/css/scheduled-tasks.css +1 -1
- package/lib/public/css/settings-ui.css +295 -0
- package/lib/public/css/shared-browser-skins.css +28 -0
- package/lib/public/css/shared-browser.css +91 -0
- package/lib/public/css/sticky-notes.css +0 -11
- package/lib/public/index.html +50 -22
- package/lib/public/modules/app-messages.js +6 -0
- package/lib/public/modules/app-projects.js +1 -1
- package/lib/public/modules/extension-settings.js +110 -0
- package/lib/public/modules/issues.js +4 -1
- package/lib/public/modules/linear-panel.js +120 -0
- package/lib/public/modules/linear-settings.js +61 -0
- package/lib/public/modules/mcp-skills-workbench.js +67 -0
- package/lib/public/modules/mcp-ui.js +9 -12
- package/lib/public/modules/notifications.js +2 -107
- package/lib/public/modules/project-settings.js +19 -1
- package/lib/public/modules/session-github.js +15 -5
- package/lib/public/modules/shared-browser-activity.js +66 -0
- package/lib/public/modules/shared-browser-input.js +73 -0
- package/lib/public/modules/shared-browser-tabs.js +33 -0
- package/lib/public/modules/shared-browser-viewport-menu.js +76 -0
- package/lib/public/modules/shared-browser.js +214 -0
- package/lib/public/modules/sidebar-mobile.js +3 -3
- package/lib/public/modules/skills-actions.js +247 -0
- package/lib/public/modules/skills-detail.js +148 -0
- package/lib/public/modules/skills-state.js +10 -0
- package/lib/public/modules/skills.js +64 -459
- package/lib/public/modules/sticky-notes.js +23 -6
- package/lib/public/modules/tool-palette-order.js +11 -13
- package/lib/public/modules/user-settings.js +4 -0
- package/lib/public/style.css +8 -0
- package/lib/sdk-bridge.js +22 -1
- package/lib/server-linear.js +50 -0
- package/lib/server.js +5 -0
- package/lib/sessions.js +7 -0
- package/lib/shared-browser-activity-history.js +17 -0
- package/lib/shared-browser-policy.js +26 -0
- package/lib/shared-browser-runtime.js +82 -0
- package/lib/shared-browser-tools.js +21 -0
- package/lib/shared-browser-worker.js +160 -0
- package/lib/ws-schema.js +10 -0
- package/lib/yoke/skill-discovery.js +22 -2
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { initExtensionSettings } from './extension-settings.js';
|
|
1
2
|
import { copyToClipboard, showToast } from './utils.js';
|
|
2
3
|
import { iconHtml, refreshIcons } from './icons.js';
|
|
3
4
|
|
|
@@ -161,113 +162,7 @@ export function initNotifications(_ctx) {
|
|
|
161
162
|
});
|
|
162
163
|
})();
|
|
163
164
|
|
|
164
|
-
|
|
165
|
-
(function () {
|
|
166
|
-
var extPillWrap = $("ext-pill-wrap");
|
|
167
|
-
var extPillBtn = $("ext-pill");
|
|
168
|
-
var extPopover = $("ext-popover");
|
|
169
|
-
var extDownloadBtn = $("ext-download-btn");
|
|
170
|
-
var extDownloadStatus = $("ext-download-status");
|
|
171
|
-
var extCopyUrl = $("ext-copy-url");
|
|
172
|
-
if (!extPillWrap || !extPillBtn || !extPopover) return;
|
|
173
|
-
|
|
174
|
-
// Detect extension connection via postMessage from content.js
|
|
175
|
-
var extConnected = false;
|
|
176
|
-
var connectedBanner = $("ext-connected-banner");
|
|
177
|
-
var extDesc = $("ext-popover-desc");
|
|
178
|
-
var extDivider = $("ext-popover-divider");
|
|
179
|
-
var extGuideTitle = $("ext-popover-guide-title");
|
|
180
|
-
var extSteps = extPopover.querySelector(".ext-popover-steps");
|
|
181
|
-
|
|
182
|
-
function setExtConnected() {
|
|
183
|
-
if (extConnected) return;
|
|
184
|
-
extConnected = true;
|
|
185
|
-
extPillBtn.classList.add("ext-connected");
|
|
186
|
-
extPillBtn.innerHTML = '<i data-lucide="check"></i> Extension';
|
|
187
|
-
refreshIcons(extPillBtn);
|
|
188
|
-
if (connectedBanner) connectedBanner.classList.remove("hidden");
|
|
189
|
-
if (extDownloadBtn) extDownloadBtn.style.display = "none";
|
|
190
|
-
if (extDownloadStatus) extDownloadStatus.classList.add("hidden");
|
|
191
|
-
if (extDesc) extDesc.style.display = "none";
|
|
192
|
-
if (extDivider) extDivider.style.display = "none";
|
|
193
|
-
if (extGuideTitle) extGuideTitle.style.display = "none";
|
|
194
|
-
if (extSteps) extSteps.style.display = "none";
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
window.addEventListener("message", function (event) {
|
|
198
|
-
if (event.source !== window) return;
|
|
199
|
-
if (!event.data || event.data.source !== "clay-chrome-extension") return;
|
|
200
|
-
setExtConnected();
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
// Toggle popover
|
|
204
|
-
extPillBtn.addEventListener("click", function (e) {
|
|
205
|
-
e.stopPropagation();
|
|
206
|
-
extPopover.classList.toggle("visible");
|
|
207
|
-
refreshIcons(extPopover);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
document.addEventListener("click", function (e) {
|
|
211
|
-
if (!extPopover.contains(e.target) && e.target !== extPillBtn && !extPillBtn.contains(e.target)) {
|
|
212
|
-
extPopover.classList.remove("visible");
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
// Download button
|
|
217
|
-
if (extDownloadBtn) {
|
|
218
|
-
extDownloadBtn.addEventListener("click", function (e) {
|
|
219
|
-
e.stopPropagation();
|
|
220
|
-
extDownloadBtn.disabled = true;
|
|
221
|
-
extDownloadBtn.innerHTML = iconHtml("loader") + " Downloading...";
|
|
222
|
-
refreshIcons(extDownloadBtn);
|
|
223
|
-
var loaderIcon = extDownloadBtn.querySelector(".lucide");
|
|
224
|
-
if (loaderIcon) loaderIcon.style.animation = "spin 1s linear infinite";
|
|
225
|
-
if (extDownloadStatus) {
|
|
226
|
-
extDownloadStatus.classList.remove("hidden");
|
|
227
|
-
extDownloadStatus.textContent = "Fetching from GitHub...";
|
|
228
|
-
extDownloadStatus.style.color = "";
|
|
229
|
-
}
|
|
230
|
-
fetch("/api/extension/download").then(function (resp) {
|
|
231
|
-
if (!resp.ok) throw new Error("Download failed (" + resp.status + ")");
|
|
232
|
-
return resp.blob();
|
|
233
|
-
}).then(function (blob) {
|
|
234
|
-
var url = URL.createObjectURL(blob);
|
|
235
|
-
var a = document.createElement("a");
|
|
236
|
-
a.href = url;
|
|
237
|
-
a.download = "clay-chrome-extension.zip";
|
|
238
|
-
document.body.appendChild(a);
|
|
239
|
-
a.click();
|
|
240
|
-
document.body.removeChild(a);
|
|
241
|
-
URL.revokeObjectURL(url);
|
|
242
|
-
if (extDownloadStatus) {
|
|
243
|
-
extDownloadStatus.textContent = "Download complete!";
|
|
244
|
-
extDownloadStatus.style.color = "var(--accent)";
|
|
245
|
-
}
|
|
246
|
-
showToast("Extension downloaded");
|
|
247
|
-
}).catch(function (err) {
|
|
248
|
-
if (extDownloadStatus) {
|
|
249
|
-
extDownloadStatus.textContent = "Failed: " + err.message;
|
|
250
|
-
extDownloadStatus.style.color = "var(--danger, #e53935)";
|
|
251
|
-
}
|
|
252
|
-
showToast("Download failed");
|
|
253
|
-
}).finally(function () {
|
|
254
|
-
extDownloadBtn.disabled = false;
|
|
255
|
-
extDownloadBtn.innerHTML = iconHtml("download") + " Download Extension (.zip)";
|
|
256
|
-
refreshIcons(extDownloadBtn);
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Copy chrome://extensions URL
|
|
262
|
-
if (extCopyUrl) {
|
|
263
|
-
extCopyUrl.addEventListener("click", function (e) {
|
|
264
|
-
e.stopPropagation();
|
|
265
|
-
copyToClipboard("chrome://extensions").then(function () {
|
|
266
|
-
showToast("Copied chrome://extensions");
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
})();
|
|
165
|
+
initExtensionSettings();
|
|
271
166
|
|
|
272
167
|
// --- Settings: Check for updates ---
|
|
273
168
|
(function () {
|
|
@@ -3,6 +3,8 @@ import { refreshIcons } from './icons.js';
|
|
|
3
3
|
import { showToast } from './utils.js';
|
|
4
4
|
import { parseEmojis } from './markdown.js';
|
|
5
5
|
import { closeFileViewer } from './filebrowser.js';
|
|
6
|
+
import { holdNotesForSettings, releaseNotesForSettings } from './sticky-notes.js';
|
|
7
|
+
import { openNotesBrowser, closeNotesBrowser, isNotesBrowserOpen } from './sticky-notes-browser.js';
|
|
6
8
|
import { renderModelList, renderModeList, renderEffortBar, renderThinkingBar, renderBetaCard } from './settings-defaults.js';
|
|
7
9
|
import { initProjectSessionVisibilitySettings, populateProjectSessionVisibilitySettings } from './project-session-visibility-settings.js';
|
|
8
10
|
|
|
@@ -12,6 +14,7 @@ var navItems = null;
|
|
|
12
14
|
var sections = null;
|
|
13
15
|
var currentSlug = null;
|
|
14
16
|
var currentProject = null; // { slug, name, icon }
|
|
17
|
+
var notesBrowserReopen = false;
|
|
15
18
|
|
|
16
19
|
// Emoji categories (reuse from sidebar)
|
|
17
20
|
var EMOJI_CATEGORIES = null;
|
|
@@ -186,15 +189,30 @@ export function openProjectSettings(slug, project) {
|
|
|
186
189
|
// Close file viewer if open (prevent split-screen)
|
|
187
190
|
closeFileViewer();
|
|
188
191
|
|
|
192
|
+
// Sticky notes must not sit over settings; remember what to restore.
|
|
193
|
+
if (panelEl.classList.contains("hidden")) {
|
|
194
|
+
notesBrowserReopen = isNotesBrowserOpen();
|
|
195
|
+
if (notesBrowserReopen) closeNotesBrowser();
|
|
196
|
+
holdNotesForSettings();
|
|
197
|
+
}
|
|
198
|
+
|
|
189
199
|
// Show panel
|
|
190
200
|
panelEl.classList.remove("hidden");
|
|
191
201
|
refreshIcons();
|
|
192
202
|
}
|
|
193
203
|
|
|
194
|
-
|
|
204
|
+
// `cleanup` is for teardown while leaving the project: the notes workbench is
|
|
205
|
+
// not reopened over whatever screen comes next.
|
|
206
|
+
export function closeProjectSettings(opts) {
|
|
195
207
|
if (!panelEl) return;
|
|
208
|
+
var wasOpen = !panelEl.classList.contains("hidden");
|
|
196
209
|
panelEl.classList.add("hidden");
|
|
197
210
|
closePsEmojiPicker();
|
|
211
|
+
if (wasOpen) {
|
|
212
|
+
releaseNotesForSettings();
|
|
213
|
+
if (notesBrowserReopen && !(opts && opts.cleanup)) openNotesBrowser();
|
|
214
|
+
notesBrowserReopen = false;
|
|
215
|
+
}
|
|
198
216
|
}
|
|
199
217
|
|
|
200
218
|
export function isProjectSettingsOpen() {
|
|
@@ -9,7 +9,8 @@ function escapeHtml(value) {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
function safeLinks(session) {
|
|
12
|
-
return (session && Array.isArray(session.githubLinks) ? session.githubLinks : []).filter(function (link) {
|
|
12
|
+
return (session && Array.isArray(session.githubLinks) ? session.githubLinks : []).concat(session && Array.isArray(session.linearLinks) ? session.linearLinks : []).filter(function (link) {
|
|
13
|
+
if (link.provider === 'linear') return /^https:\/\/linear\.app\/[a-zA-Z0-9_-]+\/issue\/[A-Z][A-Z0-9_]*-[1-9][0-9]*$/.test(link.url);
|
|
13
14
|
var target = githubLinkTarget(link.url);
|
|
14
15
|
return target && (target.kind === 'issue' || target.kind === 'pull');
|
|
15
16
|
});
|
|
@@ -27,7 +28,7 @@ export function orderedGithubWork(session) {
|
|
|
27
28
|
return links;
|
|
28
29
|
}
|
|
29
30
|
function compactGithubWork(links, sessionRow) {
|
|
30
|
-
if (!sessionRow) return links.slice(0, 1);
|
|
31
|
+
if (!sessionRow && !links.some(function (link) { return link.provider === 'linear'; })) return links.slice(0, 1);
|
|
31
32
|
var latestIssue = links.find(function (link) { return link.kind === 'issue'; });
|
|
32
33
|
var latestPr = links.find(function (link) { return link.kind === 'pr'; });
|
|
33
34
|
return [latestIssue, latestPr].filter(Boolean);
|
|
@@ -37,6 +38,10 @@ export function githubWorkMarkup(session, detailed, expanded, sessionRow) {
|
|
|
37
38
|
if (!links.length) return '';
|
|
38
39
|
var displayed = detailed || expanded ? links : compactGithubWork(links, sessionRow);
|
|
39
40
|
return '<span class="session-github-links' + (detailed ? ' github-work-details' : '') + (sessionRow ? ' session-row-github-links' : '') + '">' + displayed.map(function (link) {
|
|
41
|
+
if (link.provider === 'linear') {
|
|
42
|
+
var description = link.identifier + ' · ' + link.title + ' · ' + link.stateName + (link.stale ? ' · Last known status' : '');
|
|
43
|
+
return anchor(link.url, description, iconHtml('circle-dot', 'github-work-status') + '<span class="linear-work-id">' + escapeHtml(link.identifier) + '</span><span class="linear-work-title">' + escapeHtml(link.title) + '</span><span class="linear-work-state">' + escapeHtml(link.stateName) + '</span>', 'session-github-link session-linear-link');
|
|
44
|
+
}
|
|
40
45
|
var state = ['open', 'closed', 'merged', 'draft'].indexOf(link.state) >= 0 ? link.state : 'unknown';
|
|
41
46
|
var icon = link.kind === 'pr' ? (state === 'merged' ? 'git-merge' : state === 'closed' ? 'git-pull-request-closed' : state === 'draft' ? 'git-pull-request-draft' : 'git-pull-request') : (state === 'closed' ? 'circle-check' : 'circle-dot');
|
|
42
47
|
var description = link.repository + ' · ' + label(link) + ' · ' + link.title + ' · ' + state + (link.checkedAt ? ' · Checked ' + new Date(link.checkedAt).toLocaleString() : '') + (link.stale ? ' (last known status; refresh unavailable)' : '');
|
|
@@ -50,7 +55,7 @@ export function githubWorkMarkup(session, detailed, expanded, sessionRow) {
|
|
|
50
55
|
});
|
|
51
56
|
}
|
|
52
57
|
return html;
|
|
53
|
-
}).join('') + (!detailed && !expanded && links.length > displayed.length ? '<button type="button" class="github-work-more" aria-label="Show all ' + links.length + ' linked GitHub items" aria-haspopup="dialog" aria-expanded="false" data-github-links="' + escapeHtml(JSON.stringify(safeLinks(session))) + '">+' + (links.length - displayed.length) + '</button>' : '') + '</span>';
|
|
58
|
+
}).join('') + (!detailed && !expanded && links.length > displayed.length ? '<button type="button" class="github-work-more" aria-label="Show all ' + links.length + ' linked ' + (links.some(function (item) { return item.provider === 'linear'; }) ? 'work items' : 'GitHub items') + '" aria-haspopup="dialog" aria-expanded="false" data-github-links="' + escapeHtml(JSON.stringify(safeLinks(session))) + '">+' + (links.length - displayed.length) + '</button>' : '') + '</span>';
|
|
54
59
|
}
|
|
55
60
|
export function appendGithubWork(row, session, title) {
|
|
56
61
|
var html = githubWorkMarkup(session, false, false, true);
|
|
@@ -109,6 +114,8 @@ export function refreshGithubWork() {
|
|
|
109
114
|
if (!ws || ws.readyState !== 1) return;
|
|
110
115
|
store.set({ githubWorkRefresh: { key: key, at: Date.now() } });
|
|
111
116
|
ws.send(JSON.stringify({ type: 'session_github_refresh', sessionIds: ids }));
|
|
117
|
+
var linearIds = ids.filter(function (id) { return (store.get('githubWorkSessions') || []).some(function (item) { return item.id === id && (item.linearLinks || []).length; }); });
|
|
118
|
+
if (linearIds.length) ws.send(JSON.stringify({ type: 'session_linear_refresh', sessionIds: linearIds }));
|
|
112
119
|
}
|
|
113
120
|
|
|
114
121
|
export function resetGithubWork() {
|
|
@@ -126,6 +133,8 @@ function bindGithubWorkPopover() {
|
|
|
126
133
|
if (store.get('githubWorkPopoverBound')) return;
|
|
127
134
|
store.set({ githubWorkPopoverBound: true });
|
|
128
135
|
document.addEventListener('click', function (event) {
|
|
136
|
+
var linear = event.target.closest('.session-linear-link');
|
|
137
|
+
if (linear && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey) { event.preventDefault(); event.stopPropagation(); closeGithubWorkPopover(); import('./linear-panel.js').then(function (module) { module.openLinear(linear.href); }); return; }
|
|
129
138
|
var button = event.target.closest('.github-work-more');
|
|
130
139
|
if (!button) return;
|
|
131
140
|
event.preventDefault();
|
|
@@ -141,8 +150,9 @@ function bindGithubWorkPopover() {
|
|
|
141
150
|
popup.className = 'github-work-popover';
|
|
142
151
|
popup.setAttribute('popover', 'auto');
|
|
143
152
|
popup.setAttribute('role', 'dialog');
|
|
144
|
-
|
|
145
|
-
popup.
|
|
153
|
+
var workTitle = links.some(function (link) { return link.provider === 'linear'; }) ? 'Linked work' : 'Linked GitHub work';
|
|
154
|
+
popup.setAttribute('aria-label', workTitle);
|
|
155
|
+
popup.innerHTML = '<strong>' + workTitle + '</strong>' + githubWorkMarkup({ githubLinks: links }, false, true);
|
|
146
156
|
document.body.appendChild(popup);
|
|
147
157
|
button.setAttribute('aria-expanded', 'true');
|
|
148
158
|
popup.addEventListener('toggle', function (toggle) {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
|
|
3
|
+
export function activityHtml() {
|
|
4
|
+
return '<div class="shared-browser-activity"><div class="shared-browser-caption-history" role="log" aria-live="off" aria-label="Browser caption history" tabindex="0"></div><button type="button" class="shared-browser-caption-latest" hidden>Back to live ↓</button><span class="shared-browser-caption-announcement" role="status" aria-live="polite"></span></div>';
|
|
5
|
+
}
|
|
6
|
+
function ui(patch) { store.set({ sharedBrowserUi: Object.assign({}, store.get('sharedBrowserUi'), patch) }); }
|
|
7
|
+
function renderCaptions(el, browser, active) {
|
|
8
|
+
var caption = el.querySelector('.shared-browser-activity');
|
|
9
|
+
var history = el.querySelector('.shared-browser-caption-history');
|
|
10
|
+
var latest = el.querySelector('.shared-browser-caption-latest');
|
|
11
|
+
if (!history.dataset.bound) {
|
|
12
|
+
history.dataset.bound = 'true';
|
|
13
|
+
history.addEventListener('scroll', function () {
|
|
14
|
+
var pinned = history.scrollHeight - history.clientHeight - history.scrollTop < 20;
|
|
15
|
+
ui({ captionPinned: pinned });
|
|
16
|
+
caption.dataset.reviewing = String(!pinned);
|
|
17
|
+
latest.hidden = pinned;
|
|
18
|
+
});
|
|
19
|
+
latest.onclick = function () { history.scrollTop = history.scrollHeight; ui({ captionPinned: true }); latest.hidden = true; caption.dataset.reviewing = 'false'; };
|
|
20
|
+
}
|
|
21
|
+
var entries = browser && browser.activityHistory || [];
|
|
22
|
+
if (!entries.length && active) entries = [{ id: 'ready', text: "I'm ready to work through the next step with you.", phase: 'idle' }];
|
|
23
|
+
caption.hidden = !entries.length;
|
|
24
|
+
var last = entries[entries.length - 1];
|
|
25
|
+
var key = browser && browser.id + ':' + entries.length + ':' + (last ? last.id + ':' + last.phase + ':' + last.text : 'empty');
|
|
26
|
+
var state = store.get('sharedBrowserUi') || {};
|
|
27
|
+
if (state.captionHistoryKey === key) return;
|
|
28
|
+
var fresh = state.captionBrowserId !== (browser && browser.id);
|
|
29
|
+
var pinned = fresh || state.captionPinned !== false;
|
|
30
|
+
var top = history.scrollTop;
|
|
31
|
+
history.replaceChildren();
|
|
32
|
+
entries.forEach(function (entry, index) {
|
|
33
|
+
var row = document.createElement('div'); row.className = 'shared-browser-caption-row';
|
|
34
|
+
row.dataset.age = String(Math.min(entries.length - index - 1, 3));
|
|
35
|
+
var line = document.createElement('span'); line.className = 'shared-browser-activity-text'; line.textContent = entry.text;
|
|
36
|
+
row.appendChild(line);
|
|
37
|
+
if (entry.phase === 'failed' || entry.phase === 'interrupted') {
|
|
38
|
+
var status = document.createElement('small'); status.textContent = entry.phase === 'failed' ? 'This step could not be completed.' : 'This step was interrupted.'; row.appendChild(status);
|
|
39
|
+
}
|
|
40
|
+
history.appendChild(row);
|
|
41
|
+
});
|
|
42
|
+
ui({ captionHistoryKey: key, captionBrowserId: browser && browser.id, captionPinned: pinned });
|
|
43
|
+
history.scrollTop = pinned ? history.scrollHeight : top;
|
|
44
|
+
latest.hidden = pinned;
|
|
45
|
+
caption.dataset.reviewing = String(!pinned);
|
|
46
|
+
var announcement = el.querySelector('.shared-browser-caption-announcement');
|
|
47
|
+
var text = active && last ? last.text + (last.phase === 'failed' ? ' This step could not be completed.' : '') : '';
|
|
48
|
+
if (announcement.textContent !== text) announcement.textContent = text;
|
|
49
|
+
}
|
|
50
|
+
export function renderBrowserActivity(el, browser, connected) {
|
|
51
|
+
var active = browser && browser.phase === 'live' && browser.control === 'agent' && !browser.handoff && connected;
|
|
52
|
+
renderCaptions(el, browser, active);
|
|
53
|
+
var canvas = el.querySelector('canvas');
|
|
54
|
+
var pointer = active && browser.pointer;
|
|
55
|
+
var cursor = el.querySelector('.shared-browser-pointer');
|
|
56
|
+
cursor.hidden = !pointer || pointer.width !== canvas.width || pointer.height !== canvas.height;
|
|
57
|
+
if (cursor.hidden) return;
|
|
58
|
+
cursor.style.left = (100 * pointer.x / canvas.width) + '%';
|
|
59
|
+
cursor.style.top = (100 * pointer.y / canvas.height) + '%';
|
|
60
|
+
var key = browser.id + ':' + browser.epoch + ':' + pointer.sequence;
|
|
61
|
+
if ((store.get('sharedBrowserUi') || {}).pointerAnimationKey === key) return;
|
|
62
|
+
store.set({ sharedBrowserUi: Object.assign({}, store.get('sharedBrowserUi'), { pointerAnimationKey: key }) });
|
|
63
|
+
if (pointer.clicked && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
64
|
+
cursor.querySelector('i').animate([{ opacity: .8, transform: 'scale(.3)' }, { opacity: 0, transform: 'scale(1.8)' }], { duration: 650 });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { sendBrowserInput } from './shared-browser.js';
|
|
3
|
+
|
|
4
|
+
function writable() {
|
|
5
|
+
var browser = store.get('sharedBrowser');
|
|
6
|
+
return browser && browser.control === "user" && !browser.handoff && browser.phase === "live" && store.get('connected');
|
|
7
|
+
}
|
|
8
|
+
function ui(patch) { store.set({ sharedBrowserUi: Object.assign({}, store.get('sharedBrowserUi'), patch) }); }
|
|
9
|
+
export function clearBrowserInput() {
|
|
10
|
+
var current = store.get('sharedBrowserUi') || {};
|
|
11
|
+
clearTimeout(current.moveTimer);
|
|
12
|
+
ui({ moveTimer: null, move: null, pointer: null, composing: false });
|
|
13
|
+
}
|
|
14
|
+
export function bindBrowserInput(canvas, keyboard) {
|
|
15
|
+
function point(event) {
|
|
16
|
+
var box = canvas.getBoundingClientRect();
|
|
17
|
+
var scale = Math.min(box.width / canvas.width, box.height / canvas.height);
|
|
18
|
+
var x = (event.clientX - box.left - (box.width - canvas.width * scale) / 2) / scale;
|
|
19
|
+
var y = (event.clientY - box.top - (box.height - canvas.height * scale) / 2) / scale;
|
|
20
|
+
return { x: Math.max(0, Math.min(canvas.width - 1, x)), y: Math.max(0, Math.min(canvas.height - 1, y)) };
|
|
21
|
+
}
|
|
22
|
+
function flush() {
|
|
23
|
+
var current = store.get('sharedBrowserUi') || {};
|
|
24
|
+
clearTimeout(current.moveTimer);
|
|
25
|
+
var move = current.move;
|
|
26
|
+
ui({ move: null, moveTimer: null });
|
|
27
|
+
if (move && writable()) sendBrowserInput(Object.assign({ kind: "move" }, move));
|
|
28
|
+
}
|
|
29
|
+
canvas.addEventListener("pointermove", function (event) {
|
|
30
|
+
if (!writable()) return;
|
|
31
|
+
var current = store.get('sharedBrowserUi') || {};
|
|
32
|
+
ui({ move: point(event), moveTimer: current.moveTimer || setTimeout(flush, 40) });
|
|
33
|
+
});
|
|
34
|
+
canvas.addEventListener("pointerdown", function (event) {
|
|
35
|
+
if (!writable() || event.button !== 0) return;
|
|
36
|
+
event.preventDefault(); flush();
|
|
37
|
+
canvas.setPointerCapture(event.pointerId);
|
|
38
|
+
ui({ pointer: event.pointerId });
|
|
39
|
+
keyboard.focus({ preventScroll: true });
|
|
40
|
+
sendBrowserInput(Object.assign({ kind: "down" }, point(event)));
|
|
41
|
+
});
|
|
42
|
+
function release(event) {
|
|
43
|
+
var current = store.get('sharedBrowserUi') || {};
|
|
44
|
+
if (current.pointer !== event.pointerId) return;
|
|
45
|
+
flush(); ui({ pointer: null });
|
|
46
|
+
if (writable()) sendBrowserInput(Object.assign({ kind: "up" }, point(event)));
|
|
47
|
+
if (canvas.hasPointerCapture(event.pointerId)) canvas.releasePointerCapture(event.pointerId);
|
|
48
|
+
}
|
|
49
|
+
canvas.addEventListener("pointerup", release);
|
|
50
|
+
canvas.addEventListener("pointercancel", release);
|
|
51
|
+
canvas.addEventListener("lostpointercapture", release);
|
|
52
|
+
canvas.addEventListener("wheel", function (event) {
|
|
53
|
+
if (!writable()) return;
|
|
54
|
+
event.preventDefault(); sendBrowserInput({ kind: "wheel", deltaY: event.deltaY });
|
|
55
|
+
}, { passive: false });
|
|
56
|
+
canvas.addEventListener("focus", function () { if (writable()) keyboard.focus({ preventScroll: true }); });
|
|
57
|
+
keyboard.addEventListener("compositionstart", function () { ui({ composing: true }); });
|
|
58
|
+
keyboard.addEventListener("compositionend", function () { ui({ composing: false }); if (writable() && keyboard.value) sendBrowserInput({ kind: "text", text: keyboard.value }); keyboard.value = ""; });
|
|
59
|
+
keyboard.addEventListener("input", function () {
|
|
60
|
+
if ((store.get('sharedBrowserUi') || {}).composing) return;
|
|
61
|
+
if (writable() && keyboard.value) sendBrowserInput({ kind: "text", text: keyboard.value });
|
|
62
|
+
keyboard.value = "";
|
|
63
|
+
});
|
|
64
|
+
keyboard.addEventListener("keydown", function (event) {
|
|
65
|
+
if (!writable() || event.isComposing) return;
|
|
66
|
+
var key = event.key;
|
|
67
|
+
if ((event.ctrlKey || event.metaKey) && key.toLowerCase() === "a") key = "ControlOrMeta+A";
|
|
68
|
+
else if (key === "Tab" && event.shiftKey) key = "Shift+Tab";
|
|
69
|
+
if (/^(Tab|Shift\+Tab|Enter|Backspace|Delete|Escape|ArrowLeft|ArrowRight|ArrowUp|ArrowDown|Home|End|ControlOrMeta\+A)$/.test(key)) {
|
|
70
|
+
event.preventDefault(); sendBrowserInput({ kind: "key", key: key });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
|
|
3
|
+
export function renderBrowserTabs(el, select, send) {
|
|
4
|
+
var bar = el.querySelector('.shared-browser-tabs');
|
|
5
|
+
var tabs = store.get('sharedBrowsers') || [];
|
|
6
|
+
var selected = store.get('sharedBrowser');
|
|
7
|
+
var key = JSON.stringify([tabs.map(function (tab) { return [tab.id, tab.url, tab.control, tab.phase]; }), selected && selected.id, store.get('connected')]);
|
|
8
|
+
if (bar.dataset.key === key) return;
|
|
9
|
+
bar.dataset.key = key;
|
|
10
|
+
bar.replaceChildren();
|
|
11
|
+
tabs.forEach(function (tab, index) {
|
|
12
|
+
var item = document.createElement('div'); item.className = 'shared-browser-tab';
|
|
13
|
+
var button = document.createElement('button'); button.type = 'button'; button.setAttribute('role', 'tab');
|
|
14
|
+
button.setAttribute('aria-selected', String(!!selected && selected.id === tab.id));
|
|
15
|
+
var title = 'Tab ' + (index + 1);
|
|
16
|
+
try { if (tab.url) title = new URL(tab.url).hostname; } catch (e) {}
|
|
17
|
+
var label = document.createElement('span'); label.textContent = title;
|
|
18
|
+
var owner = document.createElement('small'); owner.textContent = tab.phase === 'ended' ? 'Ended' : tab.control === 'agent' ? 'Clay' : 'You';
|
|
19
|
+
button.append(label, owner); button.title = title + ' · ' + owner.textContent;
|
|
20
|
+
button.onclick = function () { select(tab.id); };
|
|
21
|
+
button.onkeydown = function (event) {
|
|
22
|
+
var next = event.key === 'ArrowRight' ? (index + 1) % tabs.length : event.key === 'ArrowLeft' ? (index + tabs.length - 1) % tabs.length : event.key === 'Home' ? 0 : event.key === 'End' ? tabs.length - 1 : -1;
|
|
23
|
+
if (next < 0) return;
|
|
24
|
+
event.preventDefault(); select(tabs[next].id); bar.querySelectorAll('[role="tab"]')[next].focus();
|
|
25
|
+
};
|
|
26
|
+
var close = document.createElement('button'); close.type = 'button'; close.textContent = '×'; close.setAttribute('aria-label', 'Close browser tab ' + (index + 1)); close.disabled = !store.get('connected');
|
|
27
|
+
close.onclick = function () { send('close', { browserId: tab.id }); };
|
|
28
|
+
item.append(button, close); bar.appendChild(item);
|
|
29
|
+
});
|
|
30
|
+
var add = document.createElement('button'); add.type = 'button'; add.textContent = '+'; add.setAttribute('aria-label', 'New browser tab'); add.disabled = !store.get('connected');
|
|
31
|
+
add.onclick = function () { send('open', { newTab: true }); };
|
|
32
|
+
bar.appendChild(add);
|
|
33
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { iconHtml } from './icons.js';
|
|
3
|
+
import { sendBrowserInput } from './shared-browser.js';
|
|
4
|
+
|
|
5
|
+
var presets = ['1280,800', '1440,900', '1024,768', '800,600', '375,667', '390,844'];
|
|
6
|
+
function root() { return document.getElementById('shared-browser-resolution'); }
|
|
7
|
+
function setOpen(value) {
|
|
8
|
+
store.set({ sharedBrowserUi: Object.assign({}, store.get('sharedBrowserUi'), { viewportMenuOpen: value }) });
|
|
9
|
+
var el = root();
|
|
10
|
+
if (!el) return;
|
|
11
|
+
el.querySelector('[data-viewport-trigger]').setAttribute('aria-expanded', String(value));
|
|
12
|
+
el.querySelector('[role="menu"]').hidden = !value;
|
|
13
|
+
}
|
|
14
|
+
export function closeViewportMenu() { setOpen(false); }
|
|
15
|
+
export function viewportMenuHtml() {
|
|
16
|
+
return '<div id="shared-browser-resolution" class="shared-browser-resolution"><button type="button" data-viewport-trigger aria-label="Browser viewport" aria-haspopup="menu" aria-expanded="false" aria-controls="shared-browser-resolutions"><span>1280 × 800</span>' + iconHtml('chevron-up') + '</button><div id="shared-browser-resolutions" class="shared-browser-resolution-menu" role="menu" aria-label="Browser resolutions" hidden></div></div>';
|
|
17
|
+
}
|
|
18
|
+
export function renderViewportMenu(panel, browser, enabled) {
|
|
19
|
+
var el = panel.querySelector('.shared-browser-resolution');
|
|
20
|
+
var trigger = el.querySelector('[data-viewport-trigger]');
|
|
21
|
+
trigger.disabled = !enabled;
|
|
22
|
+
if (!enabled || !store.get('sharedBrowserOpen')) closeViewportMenu();
|
|
23
|
+
var size = browser ? browser.width + ',' + browser.height : '1280,800';
|
|
24
|
+
trigger.querySelector('span').textContent = size.replace(',', ' × ');
|
|
25
|
+
var menu = el.querySelector('[role="menu"]');
|
|
26
|
+
if (menu.dataset.size === size) return;
|
|
27
|
+
menu.dataset.size = size;
|
|
28
|
+
var values = presets.includes(size) ? presets : presets.concat([size]);
|
|
29
|
+
menu.replaceChildren();
|
|
30
|
+
values.forEach(function (value) {
|
|
31
|
+
var option = document.createElement('button');
|
|
32
|
+
option.type = 'button'; option.setAttribute('role', 'menuitemradio');
|
|
33
|
+
option.setAttribute('aria-checked', String(value === size));
|
|
34
|
+
option.dataset.size = value; option.tabIndex = -1;
|
|
35
|
+
option.textContent = value.replace(',', ' × ');
|
|
36
|
+
option.setAttribute('aria-label', option.textContent);
|
|
37
|
+
menu.appendChild(option);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export function bindViewportMenu(panel) {
|
|
41
|
+
var el = panel.querySelector('.shared-browser-resolution');
|
|
42
|
+
var trigger = el.querySelector('[data-viewport-trigger]');
|
|
43
|
+
var menu = el.querySelector('[role="menu"]');
|
|
44
|
+
function open(last) {
|
|
45
|
+
if (trigger.disabled) return;
|
|
46
|
+
setOpen(true);
|
|
47
|
+
var items = menu.querySelectorAll('button');
|
|
48
|
+
var selected = menu.querySelector('[aria-checked="true"]');
|
|
49
|
+
(last ? items[items.length - 1] : selected || items[0]).focus();
|
|
50
|
+
}
|
|
51
|
+
trigger.onclick = function () {
|
|
52
|
+
if ((store.get('sharedBrowserUi') || {}).viewportMenuOpen) closeViewportMenu(); else open(false);
|
|
53
|
+
};
|
|
54
|
+
trigger.onkeydown = function (event) {
|
|
55
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { event.preventDefault(); open(event.key === 'ArrowUp'); }
|
|
56
|
+
};
|
|
57
|
+
menu.onclick = function (event) {
|
|
58
|
+
var option = event.target.closest('[data-size]');
|
|
59
|
+
if (!option || trigger.disabled) return;
|
|
60
|
+
var size = option.dataset.size.split(',');
|
|
61
|
+
closeViewportMenu(); trigger.focus();
|
|
62
|
+
sendBrowserInput({ kind: 'resize', width: Number(size[0]), height: Number(size[1]) });
|
|
63
|
+
};
|
|
64
|
+
el.addEventListener('keydown', function (event) {
|
|
65
|
+
if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); closeViewportMenu(); trigger.focus(); return; }
|
|
66
|
+
if (event.key === 'Tab') { closeViewportMenu(); return; }
|
|
67
|
+
if (event.target === trigger || !['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return;
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
var items = Array.from(menu.querySelectorAll('button'));
|
|
70
|
+
var index = items.indexOf(document.activeElement);
|
|
71
|
+
index = event.key === 'Home' ? 0 : event.key === 'End' ? items.length - 1 : (index + (event.key === 'ArrowDown' ? 1 : -1) + items.length) % items.length;
|
|
72
|
+
items[index].focus();
|
|
73
|
+
});
|
|
74
|
+
document.addEventListener('pointerdown', function (event) { if (!el.contains(event.target)) closeViewportMenu(); });
|
|
75
|
+
el.addEventListener('focusout', function (event) { if (!el.contains(event.relatedTarget)) closeViewportMenu(); });
|
|
76
|
+
}
|