clay-server 4.8.0-beta.1 → 4.8.0-beta.2

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 (44) 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/project-http.js +14 -5
  5. package/lib/project-shared-browser.js +256 -0
  6. package/lib/project.js +35 -1
  7. package/lib/public/app.js +7 -0
  8. package/lib/public/css/filebrowser.css +2 -25
  9. package/lib/public/css/issues.css +1 -14
  10. package/lib/public/css/mcp-skills-workbench.css +51 -0
  11. package/lib/public/css/project-logs.css +0 -11
  12. package/lib/public/css/right-workbench.css +28 -0
  13. package/lib/public/css/scheduled-tasks.css +1 -1
  14. package/lib/public/css/shared-browser-skins.css +28 -0
  15. package/lib/public/css/shared-browser.css +91 -0
  16. package/lib/public/css/sticky-notes.css +0 -11
  17. package/lib/public/index.html +30 -22
  18. package/lib/public/modules/app-messages.js +2 -0
  19. package/lib/public/modules/extension-settings.js +110 -0
  20. package/lib/public/modules/mcp-skills-workbench.js +67 -0
  21. package/lib/public/modules/mcp-ui.js +9 -12
  22. package/lib/public/modules/notifications.js +2 -107
  23. package/lib/public/modules/shared-browser-activity.js +66 -0
  24. package/lib/public/modules/shared-browser-input.js +73 -0
  25. package/lib/public/modules/shared-browser-tabs.js +33 -0
  26. package/lib/public/modules/shared-browser-viewport-menu.js +76 -0
  27. package/lib/public/modules/shared-browser.js +214 -0
  28. package/lib/public/modules/sidebar-mobile.js +3 -3
  29. package/lib/public/modules/skills-actions.js +247 -0
  30. package/lib/public/modules/skills-detail.js +148 -0
  31. package/lib/public/modules/skills-state.js +10 -0
  32. package/lib/public/modules/skills.js +64 -459
  33. package/lib/public/modules/tool-palette-order.js +11 -13
  34. package/lib/public/style.css +6 -0
  35. package/lib/sdk-bridge.js +14 -1
  36. package/lib/sessions.js +2 -0
  37. package/lib/shared-browser-activity-history.js +17 -0
  38. package/lib/shared-browser-policy.js +26 -0
  39. package/lib/shared-browser-runtime.js +82 -0
  40. package/lib/shared-browser-tools.js +21 -0
  41. package/lib/shared-browser-worker.js +160 -0
  42. package/lib/ws-schema.js +10 -0
  43. package/lib/yoke/skill-discovery.js +22 -2
  44. 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 () {
@@ -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
+ }
@@ -0,0 +1,214 @@
1
+ import { renderBrowserTabs } from './shared-browser-tabs.js';
2
+ import { store } from './store.js';
3
+ import { getWs } from './ws-ref.js';
4
+ import { iconHtml, refreshIcons } from './icons.js';
5
+ import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench } from './right-workbench.js';
6
+ import { bindBrowserInput, clearBrowserInput } from './shared-browser-input.js';
7
+ import { activityHtml, renderBrowserActivity } from './shared-browser-activity.js';
8
+ import { viewportMenuHtml, bindViewportMenu, renderViewportMenu, closeViewportMenu } from './shared-browser-viewport-menu.js';
9
+
10
+ function desktop() { return window.matchMedia('(min-width: 1024px)').matches; }
11
+ function eligible() {
12
+ var permissions = store.get('permissions');
13
+ return desktop() && !!store.get('currentSlug') && !!store.get('activeSessionId') && !store.get('dmMode') && (!permissions || permissions.terminal !== false);
14
+ }
15
+ function panel() { return document.getElementById('shared-browser-panel'); }
16
+ function ui(patch) { store.set({ sharedBrowserUi: Object.assign({}, store.get('sharedBrowserUi'), patch) }); }
17
+ function send(type, fields) {
18
+ var ws = getWs();
19
+ if (!ws || ws.readyState !== 1 || !store.get('activeSessionId')) return false;
20
+ var browser = store.get('sharedBrowser');
21
+ ws.send(JSON.stringify(Object.assign({ type: 'shared_browser_' + type, sessionId: store.get('activeSessionId'),
22
+ browserId: browser && browser.id, epoch: browser && browser.epoch }, fields || {})));
23
+ return true;
24
+ }
25
+ export function sendBrowserInput(event) { ui({ error: '' }); send('input', { event: event }); }
26
+ function layoutMonitor() {
27
+ var el = panel();
28
+ if (!el || !store.get('sharedBrowserOpen')) return;
29
+ var stage = el.querySelector('.shared-browser-viewport');
30
+ var canvas = el.querySelector('canvas');
31
+ var skin = canvas.width === 800 && canvas.height === 600 ? 'mac' : canvas.width === 1024 && canvas.height === 768 ? 'crt' : canvas.width < canvas.height ? 'phone' : 'laptop';
32
+ stage.dataset.skin = skin;
33
+ var scale = Math.min((stage.clientWidth - 96) / canvas.width, (stage.clientHeight - 140 - el.querySelector('.shared-browser-caption').offsetHeight) / canvas.height, 1);
34
+ if (scale <= 0) return;
35
+ var monitor = el.querySelector('.shared-browser-monitor');
36
+ monitor.style.width = Math.floor(canvas.width * scale) + 'px';
37
+ monitor.style.height = Math.floor(canvas.height * scale) + 'px';
38
+ el.querySelector('.shared-browser-monitor-label').textContent = canvas.width + ' × ' + canvas.height + ' · ' + Math.round(scale * 100) + '%';
39
+ }
40
+ function iconButton(action, icon, label) {
41
+ return '<button type="button" data-action="' + action + '" title="' + label + '" aria-label="' + label + '">' + iconHtml(icon) + '</button>';
42
+ }
43
+ function ensurePanel() {
44
+ if (panel()) return;
45
+ var root = document.getElementById('main-panels');
46
+ if (!root) return;
47
+ var el = document.createElement('section');
48
+ el.id = 'shared-browser-panel';
49
+ el.className = 'shared-browser-panel hidden';
50
+ el.setAttribute('aria-label', 'Shared browser');
51
+ el.innerHTML = '<header class="shared-browser-header"><span class="shared-browser-title">' + iconHtml('globe') + 'Browser</span><span class="shared-browser-phase" role="status">Ready</span><div class="shared-browser-window">' +
52
+ iconButton('wide', 'chevrons-left-right', 'Widen browser panel') + iconButton('fullscreen', 'maximize-2', 'Toggle browser fullscreen') + iconButton('hide', 'x', 'Hide browser panel') + '</div></header>' +
53
+ '<div class="shared-browser-tabs" role="tablist" aria-label="Browser tabs"></div><form class="shared-browser-address">' + iconButton('back', 'arrow-left', 'Back') + iconButton('forward', 'arrow-right', 'Forward') + iconButton('reload', 'rotate-cw', 'Reload') +
54
+ '<input aria-label="Browser address" placeholder="Enter a URL" type="text" autocomplete="off" spellcheck="false"><button type="submit" aria-label="Open address">' + iconHtml('arrow-right') + '</button></form>' +
55
+ '<div class="shared-browser-viewport"><div class="shared-browser-monitor"><div class="shared-browser-pointer-layer" aria-hidden="true"><div class="shared-browser-pointer" hidden><svg viewBox="0 0 24 28"><path d="M3 2 L3 22 L8 17 L12 26 L16 24 L12 15 L20 15 Z"/></svg><i></i></div></div><canvas width="1280" height="800" tabindex="0" aria-label="Shared browser page"></canvas>' +
56
+ '<div class="shared-browser-empty">' + iconHtml('globe') + '<h3>A browser you can share with your Driver</h3><p>Enter an address to browse together.<br>Localhost connects to the Clay host.</p><p class="shared-browser-private">Cookies last only for this browser session.</p></div>' +
57
+ '<textarea class="shared-browser-keyboard" aria-label="Type into shared browser" autocomplete="off" autocapitalize="off" spellcheck="false"></textarea><span class="shared-browser-monitor-brand" aria-hidden="true">Clay Studio</span><span class="shared-browser-monitor-detail" aria-hidden="true"></span><span class="shared-browser-monitor-stand" aria-hidden="true"></span></div><div class="shared-browser-caption"><span class="shared-browser-monitor-label"></span>' + activityHtml() + '</div></div>' +
58
+ '<p class="shared-browser-error hidden" role="status"></p>' +
59
+ '<footer class="shared-browser-footer"><div class="shared-browser-control"><span>You’re in control</span><button type="button" data-action="control">Give control</button></div>' +
60
+ '<div class="shared-browser-details">' + viewportMenuHtml() + '<button type="button" data-action="end">End browser</button></div></footer>';
61
+ root.appendChild(el);
62
+ el.querySelector('[data-action="hide"]').onclick = hideSharedBrowser;
63
+ el.querySelector('[data-action="wide"]').onclick = function () { var wide = !(store.get('sharedBrowserUi') || {}).wide; ui({ wide: wide }); render(); };
64
+ el.querySelector('[data-action="fullscreen"]').onclick = function () { var full = !(store.get('sharedBrowserUi') || {}).fullscreen; ui({ fullscreen: full }); render(); };
65
+ ['back', 'forward', 'reload'].forEach(function (action) { el.querySelector('[data-action="' + action + '"]').onclick = function () { sendBrowserInput({ kind: action }); }; });
66
+ el.querySelector('form').onsubmit = function (event) { event.preventDefault(); sendBrowserInput({ kind: 'navigate', url: el.querySelector('form input').value }); };
67
+ el.querySelector('[data-action="control"]').onclick = function () {
68
+ var browser = store.get('sharedBrowser');
69
+ if (browser) send('control', { control: browser.control === 'user' ? 'agent' : 'user' });
70
+ };
71
+ el.querySelector('[data-action="end"]').onclick = function () { var browser = store.get('sharedBrowser'); send(browser && browser.phase === 'ended' ? 'open' : 'end'); };
72
+ bindViewportMenu(el);
73
+ var observer = new ResizeObserver(layoutMonitor);
74
+ observer.observe(el.querySelector('.shared-browser-viewport'));
75
+ bindBrowserInput(el.querySelector('canvas'), el.querySelector('textarea'));
76
+ refreshIcons();
77
+ }
78
+ function renderEntry() {
79
+ var button = document.getElementById('shared-browser-btn');
80
+ if (!button) return;
81
+ button.classList.toggle('hidden', !eligible());
82
+ var browser = store.get('sharedBrowser');
83
+ button.title = browser ? 'View browser · ' + (store.get('connected') ? browser.phase : 'Disconnected') : 'Open browser';
84
+ button.setAttribute('aria-label', browser ? 'View browser' : 'Open browser');
85
+ button.setAttribute('aria-expanded', String(!!store.get('sharedBrowserOpen')));
86
+ button.disabled = !store.get('connected');
87
+ }
88
+ function render() {
89
+ renderEntry();
90
+ var el = panel();
91
+ if (!el) return;
92
+ renderBrowserTabs(el, selectBrowserTab, send);
93
+ var browser = store.get('sharedBrowser');
94
+ var state = store.get('sharedBrowserUi') || {};
95
+ var connected = store.get('connected');
96
+ var live = browser && browser.phase === 'live';
97
+ var user = live && !browser.handoff && browser.control === 'user' && connected;
98
+ el.classList.toggle('hidden', !store.get('sharedBrowserOpen') || !eligible());
99
+ el.classList.toggle('shared-browser-wide', !!state.wide);
100
+ el.classList.toggle('panel-fullscreen', !!state.fullscreen);
101
+ el.querySelector('[data-action="wide"]').setAttribute('aria-pressed', state.wide ? 'true' : 'false');
102
+ el.querySelector('[data-action="fullscreen"]').setAttribute('aria-pressed', state.fullscreen ? 'true' : 'false');
103
+ el.querySelector('.shared-browser-phase').textContent = !connected ? 'Disconnected' : browser ? ({ starting: 'Starting…', live: 'Live', ended: 'Ended · Last frame' })[browser.phase] : 'Ready';
104
+ el.querySelector('.shared-browser-phase').dataset.live = live && connected ? 'true' : 'false';
105
+ el.querySelector('.shared-browser-monitor').dataset.live = live && connected ? 'true' : 'false';
106
+ var address = el.querySelector('form input');
107
+ if (document.activeElement !== address) address.value = browser ? browser.url : '';
108
+ el.querySelectorAll('form input, form button').forEach(function (node) { node.disabled = !user; });
109
+ el.querySelector('textarea').disabled = !user;
110
+ el.querySelector('canvas').style.cursor = user ? 'default' : 'not-allowed';
111
+ el.querySelector('.shared-browser-control span').textContent = !live ? (browser && browser.phase === 'starting' ? 'Starting…' : 'Ended') : browser.handoff ? 'Switching…' : browser.control === 'user' ? 'You control' : 'Driver controls';
112
+ el.querySelector('[data-action="control"]').textContent = browser && browser.control === 'agent' ? 'Take control' : 'Give control';
113
+ el.querySelector('[data-action="control"]').disabled = !live || !!browser.handoff || !connected;
114
+ el.querySelector('[data-action="control"]').hidden = !live;
115
+ el.querySelector('[data-action="end"]').disabled = !browser || !connected;
116
+ el.querySelector('[data-action="end"]').textContent = browser && browser.phase === 'ended' ? 'New browser' : 'End browser';
117
+ var error = state.error || (browser && browser.error) || '';
118
+ el.querySelector('.shared-browser-error').textContent = error;
119
+ el.querySelector('.shared-browser-error').classList.toggle('hidden', !error);
120
+ el.querySelector('.shared-browser-empty').classList.toggle('hidden', !!(browser && browser.url && state.hasFrame));
121
+ renderViewportMenu(el, browser, user);
122
+ renderBrowserActivity(el, browser, connected);
123
+ layoutMonitor();
124
+ }
125
+ function setBrowserTab(browser) {
126
+ var old = store.get('sharedBrowser');
127
+ store.set({ sharedBrowser: browser });
128
+ if (!old || !browser || old.id !== browser.id) {
129
+ clearBrowserInput(); closeViewportMenu(); ui({ hasFrame: false, error: '', decoding: false, captionHistoryKey: null });
130
+ var el = panel(); if (el) el.querySelector('canvas').getContext('2d').clearRect(0, 0, 1920, 1200);
131
+ } else if (old.epoch !== browser.epoch) clearBrowserInput();
132
+ }
133
+ function selectBrowserTab(id) {
134
+ var browser = (store.get('sharedBrowsers') || []).find(function (tab) { return tab.id === id; });
135
+ if (!browser) return;
136
+ setBrowserTab(browser);
137
+ send('view', { visible: desktop() && !document.hidden }); render();
138
+ }
139
+ export function openSharedBrowser() {
140
+ if (!eligible()) return;
141
+ ensurePanel();
142
+ claimRightWorkbench('browser');
143
+ store.set({ sharedBrowserOpen: true });
144
+ var browser = store.get('sharedBrowser');
145
+ if (!browser) send('open');
146
+ else send('view', { visible: true });
147
+ render();
148
+ }
149
+ export function hideSharedBrowser() {
150
+ closeViewportMenu();
151
+ clearBrowserInput();
152
+ if (store.get('sharedBrowserOpen')) send('view', { visible: false });
153
+ store.set({ sharedBrowserOpen: false });
154
+ releaseRightWorkbench('browser');
155
+ render();
156
+ }
157
+ export function handleSharedBrowserMessage(msg) {
158
+ if (!msg.type || msg.type.indexOf('shared_browser_') !== 0) return false;
159
+ if (String(msg.sessionId) !== String(store.get('activeSessionId'))) return true;
160
+ if (msg.type === 'shared_browser_state') {
161
+ var old = store.get('sharedBrowser');
162
+ var tabs = msg.browsers || (msg.browser ? [msg.browser] : []);
163
+ store.set({ sharedBrowsers: tabs });
164
+ var selected = !msg.focus && old && tabs.find(function (tab) { return tab.id === old.id; });
165
+ selected = selected || msg.browser || tabs[0] || null;
166
+ setBrowserTab(selected);
167
+ if (msg.present && eligible()) {
168
+ ensurePanel(); claimRightWorkbench('browser'); store.set({ sharedBrowserOpen: true });
169
+ }
170
+ if (store.get('sharedBrowserOpen') && selected) send('view', { visible: desktop() && !document.hidden });
171
+ render();
172
+ } else if (msg.type === 'shared_browser_error') { ui({ error: msg.error }); render(); }
173
+ else if (msg.type === 'shared_browser_frame') {
174
+ var current = store.get('sharedBrowser');
175
+ if (!desktop() || !store.get('sharedBrowserOpen') || !current || current.id !== msg.browserId || (store.get('sharedBrowserUi') || {}).decoding) return true;
176
+ var sessionId = store.get('activeSessionId');
177
+ ui({ decoding: true });
178
+ var image = new Image();
179
+ image.onload = function () {
180
+ var latest = store.get('sharedBrowser');
181
+ if (latest && latest.id === msg.browserId && store.get('activeSessionId') === sessionId && panel()) {
182
+ var canvas = panel().querySelector('canvas'); canvas.width = image.naturalWidth; canvas.height = image.naturalHeight;
183
+ canvas.getContext('2d').drawImage(image, 0, 0);
184
+ ui({ decoding: false, hasFrame: true }); render();
185
+ }
186
+ };
187
+ image.onerror = function () { if (store.get('activeSessionId') === sessionId) ui({ decoding: false }); };
188
+ image.src = 'data:image/jpeg;base64,' + msg.data;
189
+ }
190
+ return true;
191
+ }
192
+ export function initSharedBrowser() {
193
+ var button = document.getElementById('shared-browser-btn');
194
+ if (!button) return;
195
+ button.onclick = openSharedBrowser;
196
+ window.matchMedia('(min-width: 1024px)').addEventListener('change', function () { if (!desktop()) hideSharedBrowser(); renderEntry(); });
197
+ document.addEventListener('visibilitychange', function () { if (store.get('sharedBrowserOpen')) send('view', { visible: !document.hidden && desktop() }); });
198
+ refreshIcons(); renderEntry();
199
+ if (store.get('connected')) send('state_request');
200
+ }
201
+ registerRightWorkbench('browser', hideSharedBrowser);
202
+ store.subscribe(function (state, previous) {
203
+ if (state.currentSlug !== previous.currentSlug || state.activeSessionId !== previous.activeSessionId || state.myUserId !== previous.myUserId) {
204
+ clearBrowserInput(); releaseRightWorkbench('browser');
205
+ store.set({ sharedBrowser: null, sharedBrowsers: [], sharedBrowserOpen: false, sharedBrowserUi: {} });
206
+ if (state.connected) send('state_request');
207
+ render();
208
+ } else if (state.connected !== previous.connected) {
209
+ if (state.connected) send('state_request');
210
+ render();
211
+ } else if (state.permissions !== previous.permissions || state.dmMode !== previous.dmMode) {
212
+ if (!eligible()) hideSharedBrowser(); renderEntry();
213
+ }
214
+ });
@@ -983,7 +983,7 @@ function renderSheetMateProfile(listEl) {
983
983
  var actions = [
984
984
  { icon: "book-open", label: "Knowledge", btnId: "mate-knowledge-btn", countId: "mate-knowledge-count" },
985
985
  { icon: "sticky-note", label: "Sticky Notes", btnId: "sticky-notes-sidebar-btn", countId: "sticky-notes-sidebar-count" },
986
- { icon: "puzzle", label: "Skills", btnId: "mate-skills-btn" }
986
+ { icon: "puzzle", label: "MCP / Skills", btnId: "mate-mcp-btn" }
987
987
  ];
988
988
 
989
989
  for (var i = 0; i < actions.length; i++) {
@@ -1100,7 +1100,7 @@ function renderSheetTools(listEl) {
1100
1100
  { icon: "brain", label: "Memory", action: "mate-memory" },
1101
1101
  { icon: "book-open", label: "Knowledge", action: "mate-knowledge" },
1102
1102
  { icon: "sticky-note", label: "Sticky Notes", action: "mate-sticky" },
1103
- { icon: "puzzle", label: "Skills", action: "mate-skills" }
1103
+ { icon: "puzzle", label: "MCP / Skills", action: "mate-skills" }
1104
1104
  ] : [
1105
1105
  { icon: "folder-tree", label: "Files", action: "files" },
1106
1106
  { icon: "square-terminal", label: "Terminal", action: "terminal" },
@@ -1130,7 +1130,7 @@ function renderSheetTools(listEl) {
1130
1130
  } else if (item.action === "mate-sticky") {
1131
1131
  targetId = "mate-sticky-notes-btn";
1132
1132
  } else if (item.action === "mate-skills") {
1133
- targetId = "mate-skills-btn";
1133
+ targetId = "mate-mcp-btn";
1134
1134
  } else if (item.action === "mate-memory") {
1135
1135
  targetId = "mate-memory-btn";
1136
1136
  } else if (item.action === "mate-debate") {