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.
Files changed (60) hide show
  1. package/lib/bundled-skills/clay-agent-browser/SKILL.md +36 -0
  2. package/lib/clay-skill-install.js +33 -0
  3. package/lib/clay-skill-storage.js +49 -0
  4. package/lib/linear-api.js +62 -0
  5. package/lib/linear-store.js +52 -0
  6. package/lib/project-http.js +14 -5
  7. package/lib/project-linear-launch.js +70 -0
  8. package/lib/project-session-linear.js +197 -0
  9. package/lib/project-shared-browser.js +256 -0
  10. package/lib/project.js +74 -2
  11. package/lib/public/app.js +7 -0
  12. package/lib/public/css/filebrowser.css +2 -25
  13. package/lib/public/css/issues.css +1 -14
  14. package/lib/public/css/linear.css +35 -0
  15. package/lib/public/css/mcp-skills-workbench.css +51 -0
  16. package/lib/public/css/project-logs.css +0 -11
  17. package/lib/public/css/right-workbench.css +28 -0
  18. package/lib/public/css/scheduled-tasks.css +1 -1
  19. package/lib/public/css/settings-ui.css +295 -0
  20. package/lib/public/css/shared-browser-skins.css +28 -0
  21. package/lib/public/css/shared-browser.css +91 -0
  22. package/lib/public/css/sticky-notes.css +0 -11
  23. package/lib/public/index.html +50 -22
  24. package/lib/public/modules/app-messages.js +6 -0
  25. package/lib/public/modules/app-projects.js +1 -1
  26. package/lib/public/modules/extension-settings.js +110 -0
  27. package/lib/public/modules/issues.js +4 -1
  28. package/lib/public/modules/linear-panel.js +120 -0
  29. package/lib/public/modules/linear-settings.js +61 -0
  30. package/lib/public/modules/mcp-skills-workbench.js +67 -0
  31. package/lib/public/modules/mcp-ui.js +9 -12
  32. package/lib/public/modules/notifications.js +2 -107
  33. package/lib/public/modules/project-settings.js +19 -1
  34. package/lib/public/modules/session-github.js +15 -5
  35. package/lib/public/modules/shared-browser-activity.js +66 -0
  36. package/lib/public/modules/shared-browser-input.js +73 -0
  37. package/lib/public/modules/shared-browser-tabs.js +33 -0
  38. package/lib/public/modules/shared-browser-viewport-menu.js +76 -0
  39. package/lib/public/modules/shared-browser.js +214 -0
  40. package/lib/public/modules/sidebar-mobile.js +3 -3
  41. package/lib/public/modules/skills-actions.js +247 -0
  42. package/lib/public/modules/skills-detail.js +148 -0
  43. package/lib/public/modules/skills-state.js +10 -0
  44. package/lib/public/modules/skills.js +64 -459
  45. package/lib/public/modules/sticky-notes.js +23 -6
  46. package/lib/public/modules/tool-palette-order.js +11 -13
  47. package/lib/public/modules/user-settings.js +4 -0
  48. package/lib/public/style.css +8 -0
  49. package/lib/sdk-bridge.js +22 -1
  50. package/lib/server-linear.js +50 -0
  51. package/lib/server.js +5 -0
  52. package/lib/sessions.js +7 -0
  53. package/lib/shared-browser-activity-history.js +17 -0
  54. package/lib/shared-browser-policy.js +26 -0
  55. package/lib/shared-browser-runtime.js +82 -0
  56. package/lib/shared-browser-tools.js +21 -0
  57. package/lib/shared-browser-worker.js +160 -0
  58. package/lib/ws-schema.js +10 -0
  59. package/lib/yoke/skill-discovery.js +22 -2
  60. 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
- // --- Extension pill popover ---
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
- export function closeProjectSettings() {
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
- popup.setAttribute('aria-label', 'Linked GitHub work');
145
- popup.innerHTML = '<strong>Linked GitHub work</strong>' + githubWorkMarkup({ githubLinks: links }, false, true);
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
+ }