clay-server 4.8.2 → 5.0.0-beta.1

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 (82) hide show
  1. package/LICENSES/README.md +13 -0
  2. package/LICENSES/open-iconic.txt +21 -0
  3. package/LICENSES/plantuml-salt.txt +674 -0
  4. package/lib/assets/salt-assets.json +15 -0
  5. package/lib/assets/salt-colors.json +156 -0
  6. package/lib/assets/salt-font-10.bin.gz +0 -0
  7. package/lib/assets/salt-font-13.bin.gz +0 -0
  8. package/lib/assets/salt-font-14.bin.gz +0 -0
  9. package/lib/assets/salt-font-16.bin.gz +0 -0
  10. package/lib/assets/salt-font-18.bin.gz +0 -0
  11. package/lib/assets/salt-font-20.bin.gz +0 -0
  12. package/lib/assets/salt-font-metrics.bin.gz +0 -0
  13. package/lib/assets/salt-font-metrics.json +101 -0
  14. package/lib/assets/salt-font-monospace-10.bin.gz +0 -0
  15. package/lib/assets/salt-font-monospace-13.bin.gz +0 -0
  16. package/lib/assets/salt-font-monospace-14.bin.gz +0 -0
  17. package/lib/assets/salt-font-monospace-16.bin.gz +0 -0
  18. package/lib/assets/salt-font-monospace-18.bin.gz +0 -0
  19. package/lib/assets/salt-font-monospace-20.bin.gz +0 -0
  20. package/lib/assets/salt-font-monospace.bin.gz +0 -0
  21. package/lib/assets/salt-icons.json +671 -0
  22. package/lib/bundled-skills/clay-sketch/SKILL.md +198 -0
  23. package/lib/mcp-browser-proxy.js +37 -0
  24. package/lib/mcp-connection-store.js +39 -0
  25. package/lib/mcp-network.js +46 -0
  26. package/lib/mcp-oauth.js +65 -0
  27. package/lib/mcp-proxy.js +32 -0
  28. package/lib/mcp-remote-connections.js +215 -0
  29. package/lib/mcp-session-access.js +15 -0
  30. package/lib/plantuml-renderer.js +43 -0
  31. package/lib/project-file-http.js +2 -0
  32. package/lib/project-mcp.js +130 -357
  33. package/lib/project-plantuml.js +48 -0
  34. package/lib/project-session-document.js +17 -1
  35. package/lib/project-user-message.js +2 -0
  36. package/lib/project-wireframe-http.js +88 -0
  37. package/lib/project.js +28 -45
  38. package/lib/public/app.js +7 -0
  39. package/lib/public/css/filebrowser.css +5 -0
  40. package/lib/public/css/mcp-connections.css +86 -0
  41. package/lib/public/css/skills-workbench.css +74 -0
  42. package/lib/public/css/wireframe.css +95 -0
  43. package/lib/public/index.html +16 -15
  44. package/lib/public/modules/app-messages.js +10 -2
  45. package/lib/public/modules/filebrowser.js +29 -3
  46. package/lib/public/modules/markdown.js +3 -1
  47. package/lib/public/modules/mcp-connection-view.js +136 -0
  48. package/lib/public/modules/mcp-permissions.js +122 -0
  49. package/lib/public/modules/mcp-ui.js +85 -461
  50. package/lib/public/modules/plantuml-preview.js +76 -0
  51. package/lib/public/modules/skills-actions.js +47 -79
  52. package/lib/public/modules/skills-detail.js +32 -145
  53. package/lib/public/modules/skills-list-view.js +29 -0
  54. package/lib/public/modules/skills-state.js +4 -2
  55. package/lib/public/modules/skills.js +104 -399
  56. package/lib/public/modules/split-view.js +14 -0
  57. package/lib/public/modules/vendor-login-browser.js +16 -5
  58. package/lib/public/modules/wireframe-chat.js +52 -0
  59. package/lib/public/modules/wireframe-render.js +69 -0
  60. package/lib/public/modules/wireframe-style.js +6 -0
  61. package/lib/public/modules/wireframe-viewport.js +70 -0
  62. package/lib/public/modules/wireframe-workbench.js +116 -0
  63. package/lib/public/style.css +3 -0
  64. package/lib/salt-chrome.js +36 -0
  65. package/lib/salt-hand.js +37 -0
  66. package/lib/salt-images.js +87 -0
  67. package/lib/salt-layout.js +60 -0
  68. package/lib/salt-options.js +23 -0
  69. package/lib/salt-overlays.js +45 -0
  70. package/lib/salt-parser.js +153 -0
  71. package/lib/salt-screen.js +111 -0
  72. package/lib/salt-svg-shapes.js +80 -0
  73. package/lib/salt-svg.js +164 -0
  74. package/lib/salt-text.js +112 -0
  75. package/lib/salt-tree.js +45 -0
  76. package/lib/sdk-bridge.js +5 -17
  77. package/lib/sdk-mcp-descriptors.js +2 -1
  78. package/lib/session-document-mcp-server.js +9 -0
  79. package/lib/vendor-login-browser-worker.js +18 -8
  80. package/lib/vendor-login-browser.js +10 -3
  81. package/lib/yoke/adapters/codex.js +1 -18
  82. package/package.json +6 -2
@@ -1,414 +1,119 @@
1
- import { skillState, basePath, getInstalledInfo, scopeLabel } from './skills-state.js';
1
+ import { skillState, patchSkills, basePath, content } from './skills-state.js';
2
2
  import { renderDetail } from './skills-detail.js';
3
- import { uninstallSkill } from './skills-actions.js';
3
+ import { renderSkillList, renderSkillFeedback } from './skills-list-view.js';
4
4
  export { handleSkillInstalled, handleSkillUninstalled } from './skills-actions.js';
5
5
  import { openMcpSkillsWorkbench, closeMcpSkillsWorkbench } from './mcp-skills-workbench.js';
6
- import { iconHtml, refreshIcons } from './icons.js';
7
- import { escapeHtml } from './utils.js';
8
6
  import { store } from './store.js';
9
7
 
10
- var modal;
11
- var contentEl;
12
- export function initSkills() {
13
-
14
- modal = document.getElementById("skills-modal");
15
- contentEl = document.getElementById("skills-content");
16
- var btn = document.getElementById("skills-btn");
17
- var closeBtn = document.getElementById("skills-modal-close");
18
- var backBtn = document.getElementById("skills-back-btn");
19
- var backdrop = modal ? modal.querySelector(".confirm-backdrop") : null;
20
-
21
- if (btn) {
22
- btn.addEventListener("click", function () {
23
- openSkillsModal();
24
- });
25
- }
26
-
27
- if (closeBtn) {
28
- closeBtn.addEventListener("click", closeSkillsModal);
29
- }
30
-
31
- if (backBtn) {
32
- backBtn.setAttribute("aria-label", "Back to skills");
33
- backBtn.addEventListener("click", showListView);
34
- }
35
-
36
- if (backdrop) {
37
- backdrop.addEventListener("click", closeSkillsModal);
38
- }
39
-
40
- store.subscribe(function (state, previous) {
41
- var next = state.mcpSkillsWorkbench || {}; var old = previous.mcpSkillsWorkbench || {};
42
- if (state.skillsRefreshVersion !== previous.skillsRefreshVersion && skillState().activeTab === 'installed' && skillState().currentView === 'list') loadInstalledSkills();
43
- if (next.open && next.tab === 'skills' && (!old.open || old.tab !== 'skills')) activateSkills();
44
- });
45
-
46
- // Search input
47
- var searchInput = document.getElementById("skills-search-input");
48
- var searchHint = document.getElementById("skills-search-hint");
49
- var searchClear = document.getElementById("skills-search-clear");
50
-
51
- function updateSearchControls() {
52
- var hasValue = searchInput && searchInput.value.length > 0;
53
- if (searchHint) searchHint.style.display = hasValue ? "none" : "";
54
- if (searchClear) {
55
- if (hasValue) { searchClear.classList.remove("hidden"); }
56
- else { searchClear.classList.add("hidden"); }
57
- }
58
- }
59
-
60
- if (searchInput) {
61
- searchInput.setAttribute("aria-label", "Search skills");
62
- searchInput.addEventListener("input", function () {
63
- var q = searchInput.value.trim();
64
- skillState().searchQuery = q;
65
- updateSearchControls();
66
- if (skillState().searchTimer) clearTimeout(skillState().searchTimer);
67
- if (!q) {
68
- // Clear search — restore tab view
69
- var tabsEl = modal.querySelector(".skills-tabs");
70
- if (tabsEl) tabsEl.style.display = "";
71
- loadSkills(skillState().activeTab);
72
- return;
73
- }
74
- // Hide tabs during search
75
- var tabsEl2 = modal.querySelector(".skills-tabs");
76
- if (tabsEl2) tabsEl2.style.display = "none";
77
- skillState().searchTimer = setTimeout(function () {
78
- loadSearchResults(q);
79
- }, 300);
80
- });
81
- }
82
-
83
- if (searchClear) {
84
- searchClear.addEventListener("click", function () {
85
- if (searchInput) { searchInput.value = ""; }
86
- skillState().searchQuery = "";
87
- updateSearchControls();
88
- if (skillState().searchTimer) clearTimeout(skillState().searchTimer);
89
- var tabsEl = modal.querySelector(".skills-tabs");
90
- if (tabsEl) tabsEl.style.display = "";
91
- loadSkills(skillState().activeTab);
92
- if (searchInput) searchInput.focus();
93
- });
94
- }
95
-
96
- // "/" key focuses search when modal is open
97
- document.addEventListener("keydown", function (e) {
98
- if (e.target && (e.target.matches("input, textarea") || e.target.isContentEditable)) return;
99
- if (e.key === "/" && modal && !modal.classList.contains("hidden") && skillState().currentView === "list") {
100
- if (document.activeElement !== searchInput) {
101
- e.preventDefault();
102
- if (searchInput) searchInput.focus();
103
- }
104
- }
105
- });
106
-
107
- // Tab clicks
108
- var tabs = modal ? modal.querySelectorAll(".skills-tab") : [];
109
- for (var i = 0; i < tabs.length; i++) {
110
- (function (tab) {
111
- tab.addEventListener("click", function () {
112
- var tabName = tab.dataset.tab;
113
- if (tabName === skillState().activeTab && skillState().currentView === "list" && !skillState().searchQuery) return;
114
- skillState().activeTab = tabName;
115
- skillState().currentView = "list";
116
- // Clear search when switching tabs
117
- skillState().searchQuery = "";
118
- var si = document.getElementById("skills-search-input");
119
- if (si) si.value = "";
120
- updateSearchControls();
121
- updateTabUI();
122
- loadSkills(tabName);
123
- });
124
- })(tabs[i]);
125
- }
126
-
127
- // Esc key
128
- document.addEventListener("keydown", function (e) {
129
- if (e.key === "Escape" && modal && !modal.classList.contains("hidden")) {
130
- if (skillState().currentView === "detail") {
131
- showListView();
132
- } else {
133
- closeSkillsModal();
134
- }
135
- }
136
- });
8
+ function target() { return String(store.get('currentSlug')) + ':' + String(store.get('myUserId')); }
9
+ function modal() { return document.getElementById('skills-modal'); }
10
+ function requestGuard() {
11
+ var version = (skillState().requestVersion || 0) + 1, owner = target();
12
+ patchSkills({ requestVersion: version });
13
+ return function () { return target() === owner && skillState().requestVersion === version; };
137
14
  }
138
-
139
- function fetchInstalledSkills() {
140
- return fetch(basePath() + "api/installed-skills")
141
- .then(function (res) { return res.json(); })
142
- .then(function (data) {
143
- skillState().installedSkills = data.installed || {};
144
- })
145
- .catch(function () {
146
- skillState().installedSkills = {};
147
- });
148
- }
149
-
150
- function openSkillsModal() { openMcpSkillsWorkbench("skills"); }
151
-
152
- function activateSkills() {
153
- if (!modal) return;
154
- var target = String(store.get('currentSlug')) + ':' + String(store.get('myUserId'));
155
- if (skillState().loadedFor === target) return;
156
- skillState().loadedFor = target;
157
- updateTabUI();
158
- refreshIcons(modal);
159
- skillState().currentView = "list";
160
- var backBtn = document.getElementById("skills-back-btn");
161
- if (backBtn) backBtn.classList.add("hidden");
162
- var tabsEl = modal.querySelector(".skills-tabs");
163
- if (tabsEl) tabsEl.style.display = "";
164
- var searchEl = document.getElementById("skills-search");
165
- if (searchEl) searchEl.style.display = "";
166
- var searchInput = document.getElementById("skills-search-input");
167
- if (searchInput) { searchInput.value = ""; skillState().searchQuery = ""; }
168
- // Load the current project inventory once; installation events refresh it.
169
- fetchInstalledSkills().then(function () {
170
- loadSkills(skillState().activeTab);
15
+ function json(url) { return fetch(url).then(function (res) { if (!res.ok) throw new Error('Request failed'); return res.json(); }); }
16
+ function chrome() {
17
+ var state = skillState(), detail = state.currentView === 'detail', root = modal();
18
+ root.querySelector('.skills-library-heading').hidden = detail;
19
+ document.getElementById('skills-back-btn').classList.toggle('hidden', !detail);
20
+ root.querySelector('.skills-tabs').hidden = detail;
21
+ root.querySelector('.skills-toolbar').hidden = detail;
22
+ root.querySelector('.skills-discovery-views').hidden = detail || state.activeTab === 'installed' || !!state.searchQuery;
23
+ root.querySelectorAll('.skills-tab').forEach(function (button) {
24
+ var active = button.dataset.tab === state.activeTab;
25
+ button.classList.toggle('active', active); button.setAttribute('aria-selected', String(active)); button.tabIndex = active ? 0 : -1;
26
+ });
27
+ var input = document.getElementById('skills-search-input');
28
+ input.placeholder = state.activeTab === 'installed' ? 'Search installed skills' : 'Search community skills';
29
+ input.setAttribute('aria-label', input.placeholder);
30
+ if (input.value !== state.searchQuery) input.value = state.searchQuery;
31
+ document.getElementById('skills-search-clear').classList.toggle('hidden', !state.searchQuery);
32
+ document.getElementById('skills-installed-count').textContent = Object.keys(state.installedSkills).length || '';
33
+ root.querySelectorAll('[data-discovery]').forEach(function (button) {
34
+ button.setAttribute('aria-pressed', String(button.dataset.discovery === (state.discoverySort || 'all')));
171
35
  });
172
36
  }
173
-
174
- function closeSkillsModal() {
175
- if (!modal) return;
176
- closeMcpSkillsWorkbench();
177
- }
178
-
179
- function updateTabUI() {
180
- var tabs = modal.querySelectorAll(".skills-tab");
181
- for (var i = 0; i < tabs.length; i++) {
182
- if (tabs[i].dataset.tab === skillState().activeTab) {
183
- tabs[i].classList.add("active");
184
- } else {
185
- tabs[i].classList.remove("active");
186
- }
187
- }
188
- }
189
-
190
- function loadSkills(tab) {
191
- // Always ensure tabs and search are visible when loading list view
192
- var tabsEl = modal.querySelector(".skills-tabs");
193
- if (tabsEl && !skillState().searchQuery) tabsEl.style.display = "";
194
- var searchEl = document.getElementById("skills-search");
195
- if (searchEl) searchEl.style.display = "";
196
-
197
- // Installed tab — uses local data, not skills.sh
198
- if (tab === "installed") {
199
- loadInstalledSkills();
200
- return;
201
- }
202
-
203
- // Check cache
204
- if (skillState().skillsData[tab]) {
205
- renderList(skillState().skillsData[tab], tab);
206
- return;
207
- }
208
-
209
- contentEl.innerHTML = '<div class="skills-loading"><div class="skills-spinner"></div> Loading skills...</div>';
210
-
211
- fetch("/api/skills?tab=" + encodeURIComponent(tab))
212
- .then(function (res) { return res.json(); })
213
- .then(function (data) {
214
- var skills = data.skills || [];
215
- skillState().skillsData[tab] = skills;
216
- renderList(skills, tab);
217
- })
218
- .catch(function (err) {
219
- contentEl.innerHTML = '<div class="skills-empty">Failed to load skills</div>';
220
- });
221
- }
222
-
223
- function loadInstalledSkills() {
224
- contentEl.innerHTML = '<div class="skills-loading"><div class="skills-spinner"></div> Loading installed skills...</div>';
225
-
226
- fetch(basePath() + "api/installed-skills")
227
- .then(function (res) { return res.json(); })
228
- .then(function (data) {
229
- skillState().installedSkills = data.installed || {};
230
- renderInstalledList(skillState().installedSkills);
231
- })
232
- .catch(function () {
233
- contentEl.innerHTML = '<div class="skills-empty">Failed to load installed skills</div>';
234
- });
235
- }
236
-
237
- function renderInstalledList(installed) {
238
- var names = Object.keys(installed);
239
- if (!names.length) {
240
- contentEl.innerHTML = '<div class="skills-empty">No skills installed<div class="skills-empty-hint">Browse the other tabs to discover and install skills</div></div>';
37
+ function renderList(rows) { chrome(); renderSkillList(rows || [], openSkill, function () { selectTab('discover'); }); }
38
+ function loadList(refresh) {
39
+ chrome();
40
+ var state = skillState(), valid = requestGuard();
41
+ if (state.activeTab === 'installed') {
42
+ if (!refresh && state.inventoryLoaded) { renderList(); return; }
43
+ renderSkillFeedback('Loading your skills…');
44
+ json(basePath() + 'api/installed-skills').then(function (data) {
45
+ if (!valid()) return;
46
+ patchSkills({ installedSkills: data.installed || {}, inventoryLoaded: true }); renderList();
47
+ }).catch(function () { if (valid()) renderSkillFeedback('Could not load your skills.', function () { loadList(true); }); });
241
48
  return;
242
49
  }
243
-
244
- var html = '<div class="skills-list">';
245
-
246
- for (var i = 0; i < names.length; i++) {
247
- var name = names[i];
248
- var info = installed[name];
249
- var desc = info.description || "";
250
-
251
- html += '<div class="skills-installed-item">' +
252
- '<div class="skills-installed-item-info">' +
253
- '<div class="skills-installed-item-header">' +
254
- '<span class="skills-installed-item-name">' + escapeHtml(name) + '</span>' +
255
- '<span class="skills-installed-badge">' + iconHtml("check") + ' ' + (info.source === "clay-builtin" ? "Built-in" : scopeLabel(info.scope)) + '</span>' +
256
- '</div>' +
257
- (desc ? '<div class="skills-installed-item-desc">' + escapeHtml(desc) + '</div>' : '') +
258
- '</div>' +
259
- '<div class="skills-installed-item-actions">' +
260
- (info.source === "clay-builtin" ? "" : buildUninstallButtons(name, info.scope)) +
261
- '</div>' +
262
- '</div>';
263
- }
264
-
265
- html += '</div>';
266
- contentEl.innerHTML = html;
267
- refreshIcons(contentEl);
268
-
269
- // Attach uninstall handlers
270
- var unBtns = contentEl.querySelectorAll(".skills-uninstall-btn");
271
- for (var j = 0; j < unBtns.length; j++) {
272
- (function (btn) {
273
- btn.addEventListener("click", function () {
274
- uninstallSkill(btn.dataset.skill, btn.dataset.scope);
275
- });
276
- })(unBtns[j]);
277
- }
50
+ var query = state.searchQuery, sort = state.discoverySort || 'all';
51
+ var cache = query ? state.searchCache[query] : state.skillsData[sort];
52
+ if (cache && !refresh) { renderList(cache); return; }
53
+ renderSkillFeedback(query ? 'Searching community skills…' : 'Loading community skills…');
54
+ json(query ? '/api/skills/search?q=' + encodeURIComponent(query) : '/api/skills?tab=' + sort).then(function (data) {
55
+ if (!valid()) return;
56
+ var rows = data.skills || [], update = {};
57
+ if (query) update.searchCache = Object.assign({}, skillState().searchCache, { [query]: rows });
58
+ else update.skillsData = Object.assign({}, skillState().skillsData, { [sort]: rows });
59
+ patchSkills(update); renderList(rows);
60
+ }).catch(function () { if (valid()) renderSkillFeedback('Could not load community skills.', function () { loadList(true); }); });
278
61
  }
279
-
280
- function buildUninstallButtons(skillName, scope) {
281
- var html = '';
282
- if (scope === "both") {
283
- html += '<button class="skills-uninstall-btn" data-skill="' + escapeHtml(skillName) + '" data-scope="project" title="Uninstall (Project)">' + iconHtml("x") + '</button>';
284
- html += '<button class="skills-uninstall-btn" data-skill="' + escapeHtml(skillName) + '" data-scope="global" title="Uninstall (Global)">' + iconHtml("x") + '</button>';
285
- } else {
286
- html += '<button class="skills-uninstall-btn" data-skill="' + escapeHtml(skillName) + '" data-scope="' + escapeHtml(scope) + '" title="Uninstall">' + iconHtml("x") + '</button>';
287
- }
288
- return html;
62
+ function selectTab(tab) {
63
+ clearTimeout(skillState().searchTimer);
64
+ patchSkills({ activeTab: tab, currentView: 'list', searchQuery: '', selectedSkill: null }); loadList();
289
65
  }
290
-
291
- function loadSearchResults(q) {
292
- // Stale check — if query changed since this was scheduled, skip
293
- if (q !== skillState().searchQuery) return;
294
-
295
- // Check cache
296
- if (skillState().searchCache[q]) {
297
- renderList(skillState().searchCache[q], "search");
298
- return;
299
- }
300
-
301
- contentEl.innerHTML = '<div class="skills-loading"><div class="skills-spinner"></div> Searching...</div>';
302
-
303
- fetch("/api/skills/search?q=" + encodeURIComponent(q))
304
- .then(function (res) { return res.json(); })
305
- .then(function (data) {
306
- if (q !== skillState().searchQuery) return; // stale
307
- var skills = data.skills || [];
308
- skillState().searchCache[q] = skills;
309
- renderList(skills, "search");
310
- })
311
- .catch(function (err) {
312
- if (q !== skillState().searchQuery) return;
313
- contentEl.innerHTML = '<div class="skills-empty">Search failed</div>';
314
- });
66
+ function showList() { patchSkills({ currentView: 'list', selectedSkill: null }); loadList(); document.getElementById('skills-search-input').focus(); }
67
+ function openSkill(row, installed) {
68
+ clearTimeout(skillState().searchTimer);
69
+ patchSkills({ currentView: 'detail', selectedSkill: row.skillId || row.name }); chrome();
70
+ var valid = requestGuard(); content().scrollTop = 0;
71
+ if (installed) {
72
+ renderDetail(Object.assign({}, row, { _skill: row.name, _installed: true })); return;
73
+ }
74
+ renderSkillFeedback('Loading skill details…');
75
+ json('/api/skills/detail?source=' + encodeURIComponent(row.source) + '&skill=' + encodeURIComponent(row.skillId || row.name)).then(function (data) {
76
+ if (valid()) renderDetail(Object.assign({}, data, { _source: row.source, _skill: row.skillId || row.name }));
77
+ }).catch(function () { if (valid()) renderSkillFeedback('Could not load this skill.', function () { openSkill(row, false); }); });
315
78
  }
316
-
317
- function formatInstalls(n) {
318
- if (typeof n !== "number") return n || "0";
319
- if (n >= 1000000) return (n / 1000000).toFixed(1).replace(/\.0$/, "") + "M";
320
- if (n >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, "") + "K";
321
- return n.toString();
79
+ function activate() {
80
+ if (skillState().loadedFor === target()) return;
81
+ clearTimeout(skillState().searchTimer);
82
+ patchSkills({ loadedFor: target(), activeTab: 'installed', currentView: 'list', searchQuery: '', installedSkills: {}, inventoryLoaded: false, selectedSkill: null });
83
+ loadList(true);
322
84
  }
323
-
324
- function renderList(skills, tab) {
325
- if (!skills.length) {
326
- contentEl.innerHTML = '<div class="skills-empty">' + (skillState().searchQuery ? "No matching skills" : "No skills found") + '</div>';
327
- return;
328
- }
329
-
330
- var isHot = tab === "hot";
331
- var html = '<div class="skills-list">';
332
-
333
- for (var i = 0; i < skills.length; i++) {
334
- var s = skills[i];
335
- var info = getInstalledInfo(s.skillId || s.name);
336
- var changeHtml = "";
337
- if (isHot && typeof s.change === "number" && s.change !== 0) {
338
- var cls = s.change > 0 ? "" : " negative";
339
- changeHtml = '<span class="skills-item-change' + cls + '">' +
340
- (s.change > 0 ? "+" : "") + formatInstalls(s.change) + '</span>';
341
- }
342
-
343
- var installedBadge = "";
344
- if (info) {
345
- installedBadge = '<span class="skills-installed-badge">' + iconHtml("check") + ' ' + (info.source === "clay-builtin" ? "Built-in" : scopeLabel(info.scope)) + '</span>';
346
- }
347
-
348
- html += '<div class="skills-item' + (info ? " installed" : "") + '" data-source="' + escapeHtml(s.source) + '" data-skill="' + escapeHtml(s.skillId) + '">' +
349
- '<span class="skills-item-rank">' + (i + 1) + '</span>' +
350
- '<div class="skills-item-info">' +
351
- '<div class="skills-item-name">' + escapeHtml(s.name) + installedBadge + '</div>' +
352
- '<div class="skills-item-source">' + escapeHtml(s.source) + '</div>' +
353
- '</div>' +
354
- '<div class="skills-item-stats">' +
355
- '<span class="skills-item-installs">' + formatInstalls(s.installs) + '</span>' +
356
- changeHtml +
357
- '</div>' +
358
- '</div>';
359
- }
360
-
361
- html += '</div>';
362
- contentEl.innerHTML = html;
363
- refreshIcons(contentEl);
364
-
365
- // Attach click handlers
366
- var items = contentEl.querySelectorAll(".skills-item");
367
- for (var j = 0; j < items.length; j++) {
368
- (function (item) {
369
- item.addEventListener("click", function () {
370
- var source = item.dataset.source;
371
- var skill = item.dataset.skill;
372
- loadDetail(source, skill);
373
- });
374
- })(items[j]);
375
- }
376
- }
377
-
378
- function loadDetail(source, skill) {
379
- skillState().currentView = "detail";
380
-
381
- // Hide tabs and search
382
- var tabsEl = modal.querySelector(".skills-tabs");
383
- if (tabsEl) tabsEl.style.display = "none";
384
- var searchEl = document.getElementById("skills-search");
385
- if (searchEl) searchEl.style.display = "none";
386
-
387
- contentEl.innerHTML = '<div class="skills-loading"><div class="skills-spinner"></div> Loading skill details...</div>';
388
-
389
- fetch("/api/skills/detail?source=" + encodeURIComponent(source) + "&skill=" + encodeURIComponent(skill))
390
- .then(function (res) { return res.json(); })
391
- .then(function (data) {
392
- data._source = source;
393
- data._skill = skill;
394
- renderDetail(data);
395
- })
396
- .catch(function (err) {
397
- contentEl.innerHTML = '<div class="skills-empty">Failed to load skill details</div>';
398
- });
399
- }
400
-
401
- function showListView() {
402
- skillState().currentView = "list";
403
- var tabsEl = modal.querySelector(".skills-tabs");
404
- if (tabsEl) tabsEl.style.display = skillState().searchQuery ? "none" : "";
405
- var searchEl = document.getElementById("skills-search");
406
- if (searchEl) searchEl.style.display = "";
407
- var backBtn = document.getElementById("skills-back-btn");
408
- if (backBtn) backBtn.classList.add("hidden");
409
- if (skillState().searchQuery) {
410
- loadSearchResults(skillState().searchQuery);
411
- } else {
412
- loadSkills(skillState().activeTab);
413
- }
85
+ export function initSkills() {
86
+ if (!modal()) return;
87
+ var input = document.getElementById('skills-search-input');
88
+ document.getElementById('skills-back-btn').onclick = showList;
89
+ var legacy = document.getElementById('skills-btn'); if (legacy) legacy.onclick = function () { openMcpSkillsWorkbench('skills'); };
90
+ modal().querySelectorAll('.skills-tab').forEach(function (button) {
91
+ button.onclick = function () { selectTab(button.dataset.tab); };
92
+ button.onkeydown = function (event) {
93
+ if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
94
+ event.preventDefault(); var tab = event.key === 'Home' ? 'installed' : event.key === 'End' ? 'discover' : skillState().activeTab === 'installed' ? 'discover' : 'installed';
95
+ selectTab(tab); modal().querySelector('[data-tab="' + tab + '"]').focus();
96
+ };
97
+ });
98
+ input.oninput = function () {
99
+ clearTimeout(skillState().searchTimer); requestGuard(); patchSkills({ searchQuery: input.value }); chrome();
100
+ if (skillState().activeTab === 'installed') loadList();
101
+ else patchSkills({ searchTimer: setTimeout(function () { loadList(); }, 250) });
102
+ };
103
+ document.getElementById('skills-search-clear').onclick = function () { input.value = ''; input.oninput(); input.focus(); };
104
+ modal().querySelectorAll('[data-discovery]').forEach(function (button) {
105
+ button.onclick = function () { patchSkills({ discoverySort: button.dataset.discovery }); loadList(); };
106
+ });
107
+ document.addEventListener('keydown', function (event) {
108
+ var panel = store.get('mcpSkillsWorkbench') || {};
109
+ if (!panel.open || panel.tab !== 'skills' || (event.target && (event.target.matches('input,textarea,select') || event.target.isContentEditable))) return;
110
+ if (event.key === '/' && skillState().currentView === 'list') { event.preventDefault(); input.focus(); }
111
+ if (event.key === 'Escape') { if (skillState().currentView === 'detail') showList(); else closeMcpSkillsWorkbench(); }
112
+ });
113
+ store.subscribe(function (state, previous) {
114
+ var next = state.mcpSkillsWorkbench || {}, old = previous.mcpSkillsWorkbench || {};
115
+ if (state.currentSlug !== previous.currentSlug || state.myUserId !== previous.myUserId) { clearTimeout(skillState().searchTimer); requestGuard(); }
116
+ if (state.skillsRefreshVersion !== previous.skillsRefreshVersion && skillState().currentView === 'list') loadList(true);
117
+ if (next.open && next.tab === 'skills' && (!old.open || old.tab !== 'skills')) activate();
118
+ });
414
119
  }
@@ -1,3 +1,4 @@
1
+ import { openWireframe } from './wireframe-workbench.js';
1
2
  // Two-pane iframe shell for the split-view spike.
2
3
 
3
4
  import { store } from './store.js';
@@ -142,6 +143,19 @@ function handlePaneMessage(event) {
142
143
  if (event.origin !== window.location.origin) return;
143
144
  var msg = event.data;
144
145
  if (!msg || !host) return;
146
+ if (msg.type === "clay-pane-present-wireframe") {
147
+ var frames = host.querySelectorAll(".split-pane-frame");
148
+ var split = store.get('splitPanes');
149
+ var trusted = Array.prototype.some.call(frames, function (frame) {
150
+ if (frame.contentWindow !== event.source || frame.dataset.projectSlug !== msg.projectSlug || String(frame.dataset.sessionId) !== String(msg.sessionId) || msg.projectSlug !== store.get('currentSlug')) return false;
151
+ return split && split.panes.some(function (pane) {
152
+ var expected = new URL('/p/' + encodeURIComponent(pane.slug) + '/?pane=1&session=' + encodeURIComponent(pane.sessionId), window.location.href).href;
153
+ return pane.slug === msg.projectSlug && String(pane.sessionId) === String(msg.sessionId) && frame.src === expected;
154
+ });
155
+ });
156
+ if (trusted && msg.message) openWireframe(msg.message);
157
+ return;
158
+ }
145
159
  if (msg.type === "clay-pane-present-markdown") {
146
160
  var present = msg.message;
147
161
  if (present) presentMarkdownEdit(present);
@@ -9,7 +9,8 @@ function send(type, extra) {
9
9
  var view = current();
10
10
  var ws = getWs();
11
11
  if (!view || view.slug !== store.get('currentSlug') || !ws || ws.readyState !== 1) return;
12
- ws.send(JSON.stringify(Object.assign({ type: type, vendor: view.vendor, browserId: view.browserId }, extra || {})));
12
+ if (view.kind === 'mcp') type = type.replace('vendor_login_', 'mcp_login_');
13
+ ws.send(JSON.stringify(Object.assign({ type: type, vendor: view.vendor, browserId: view.browserId, connectionId: view.connectionId }, extra || {})));
13
14
  }
14
15
 
15
16
  function input(event) { send("vendor_login_browser_input", { event: event }); }
@@ -31,9 +32,10 @@ export function openLoginBrowser(msg) {
31
32
  return;
32
33
  }
33
34
  closeLoginBrowser();
34
- var title = msg.vendor === "codex" ? "Sign in to Codex" : "Sign in to Claude";
35
+ var isMcp = msg.kind === 'mcp';
36
+ var title = isMcp ? 'Sign in to ' + (msg.label || 'MCP server') : (msg.vendor === "codex" ? "Sign in to Codex" : "Sign in to Claude");
35
37
  store.set({ vendorLoginBrowser: {
36
- browserId: msg.browserId, vendor: msg.vendor, slug: msg.slug || store.get('currentSlug'),
38
+ browserId: msg.browserId, vendor: msg.vendor, kind: msg.kind, connectionId: msg.connectionId, slug: msg.slug || store.get('currentSlug'),
37
39
  previousFocus: document.activeElement, decoding: false, pointerY: null, pointerId: null, mouseDown: false, moveTimer: null, pendingMove: null, resizeObserver: null, requestedSize: null, phase: "loading",
38
40
  } });
39
41
  var dialog = document.createElement("dialog");
@@ -60,7 +62,15 @@ export function openLoginBrowser(msg) {
60
62
  document.body.appendChild(dialog);
61
63
  var keyboard = dialog.querySelector("textarea");
62
64
  var canvas = dialog.querySelector("canvas");
63
- function cancel() { cancelVendorLogin(msg.vendor); }
65
+ if (isMcp) {
66
+ dialog.querySelector('.login-browser-vendor').hidden = true;
67
+ dialog.querySelector('[data-action="terminal"]').hidden = true;
68
+ dialog.querySelector('.login-browser-privacy').textContent = 'This private browser is controlled only by you. Cookies are discarded when sign-in ends.';
69
+ }
70
+ function cancel() {
71
+ if (isMcp) { send('vendor_login_cancel'); closeLoginBrowser(); }
72
+ else cancelVendorLogin(msg.vendor);
73
+ }
64
74
  function restart(mode) {
65
75
  var ws = getWs();
66
76
  if (!ws || ws.readyState !== 1) {
@@ -68,7 +78,8 @@ export function openLoginBrowser(msg) {
68
78
  return;
69
79
  }
70
80
  cancel();
71
- requestVendorLogin(msg.vendor, { mode: mode });
81
+ if (isMcp) ws.send(JSON.stringify({ type: 'mcp_connection_action', action: 'signin', id: msg.connectionId }));
82
+ else requestVendorLogin(msg.vendor, { mode: mode });
72
83
  }
73
84
  dialog.querySelector('[data-action="cancel"]').onclick = cancel;
74
85
  dialog.querySelector('[data-action="retry"]').onclick = function () { restart("browser"); };