claudeck 1.4.0 → 1.4.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.
- package/README.md +6 -8
- package/package.json +1 -1
- package/plugins/claude-editor/manifest.json +10 -0
- package/plugins/repos/manifest.json +10 -0
- package/public/css/core/theme.css +6 -21
- package/public/css/core/variables.css +2 -0
- package/public/css/features/message-queue.css +348 -0
- package/public/css/ui/commands.css +4 -4
- package/public/css/ui/messages.css +310 -78
- package/public/css/ui/right-panel.css +207 -0
- package/public/css/ui/sessions.css +173 -0
- package/public/css/ui/settings.css +75 -0
- package/public/index.html +10 -2
- package/public/js/components/add-project-modal.js +14 -0
- package/public/js/components/jump-to-latest.js +42 -0
- package/public/js/components/queue-stop-modal.js +23 -0
- package/public/js/components/settings-modal.js +65 -0
- package/public/js/core/api.js +15 -43
- package/public/js/core/dom.js +17 -0
- package/public/js/core/events.js +11 -0
- package/public/js/core/plugin-loader.js +96 -11
- package/public/js/core/store.js +11 -0
- package/public/js/core/utils.js +38 -2
- package/public/js/features/chat.js +49 -1
- package/public/js/features/message-queue.js +423 -0
- package/public/js/features/projects.js +185 -3
- package/public/js/main.js +4 -1
- package/public/js/panels/assistant-bot.js +16 -0
- package/public/js/panels/dev-docs.js +2 -2
- package/public/js/panels/memory.js +1 -0
- package/public/js/ui/context-gauge.js +10 -1
- package/public/js/ui/formatting.js +65 -11
- package/public/js/ui/header-dropdowns.js +30 -0
- package/public/js/ui/input-meta.js +13 -6
- package/public/js/ui/max-turns.js +6 -3
- package/public/js/ui/messages.js +97 -1
- package/public/js/ui/model-selector.js +1 -0
- package/public/js/ui/parallel.js +32 -2
- package/public/js/ui/permissions.js +1 -0
- package/public/js/ui/right-panel.js +0 -8
- package/public/js/ui/tab-sdk.js +395 -176
- package/public/style.css +2 -0
- package/server/memory-optimizer.js +17 -13
- package/server/routes/marketplace.js +316 -0
- package/server/routes/projects.js +0 -0
- package/server/ws-handler.js +22 -15
- package/server.js +18 -0
- package/plugins/event-stream/client.css +0 -207
- package/plugins/event-stream/client.js +0 -271
- package/plugins/linear/client.css +0 -345
- package/plugins/linear/client.js +0 -380
- package/plugins/linear/config.json +0 -5
- package/plugins/linear/server.js +0 -312
- package/plugins/sudoku/client.css +0 -196
- package/plugins/sudoku/client.js +0 -329
- package/plugins/tasks/client.css +0 -414
- package/plugins/tasks/client.js +0 -394
- package/plugins/tasks/server.js +0 -116
- package/plugins/tic-tac-toe/client.css +0 -167
- package/plugins/tic-tac-toe/client.js +0 -241
- package/public/js/components/linear-create-modal.js +0 -43
package/public/js/ui/tab-sdk.js
CHANGED
|
@@ -53,18 +53,26 @@
|
|
|
53
53
|
// config.onDeactivate {function} Optional. Called each time tab is hidden
|
|
54
54
|
// config.onDestroy {function} Optional. Called when tab is unregistered
|
|
55
55
|
//
|
|
56
|
-
// ── Context object (ctx) — passed to init()
|
|
56
|
+
// ── Context object (ctx) — passed to init(), onActivate, onDeactivate, onDestroy
|
|
57
57
|
//
|
|
58
|
-
// ctx.
|
|
58
|
+
// ctx.pluginId Your plugin's ID string
|
|
59
|
+
// ctx.on(event, fn) Subscribe to event bus (returns unsubscribe fn)
|
|
60
|
+
// ctx.off(event, fn) Remove an event listener
|
|
59
61
|
// ctx.emit(event, data) Publish to the app event bus
|
|
60
62
|
// ctx.getState(key) Read from the reactive store
|
|
61
|
-
// ctx.onState(key, fn) Subscribe to store changes
|
|
63
|
+
// ctx.onState(key, fn) Subscribe to store changes (returns unsubscribe fn)
|
|
62
64
|
// ctx.api The full API module (fetch helpers)
|
|
63
65
|
// ctx.getProjectPath() Current project path
|
|
64
66
|
// ctx.getSessionId() Current session ID
|
|
67
|
+
// ctx.getTheme() Current theme: 'dark' or 'light'
|
|
68
|
+
// ctx.storage.get(key) Read from plugin-scoped localStorage
|
|
69
|
+
// ctx.storage.set(key, val) Write to plugin-scoped localStorage
|
|
70
|
+
// ctx.storage.remove(key) Remove from plugin-scoped localStorage
|
|
71
|
+
// ctx.toast(msg, opts) Show a temporary notification (opts: {duration, type})
|
|
65
72
|
// ctx.showBadge(count) Show a number badge on the tab button
|
|
66
73
|
// ctx.clearBadge() Hide the badge
|
|
67
74
|
// ctx.setTitle(text) Update the tab button label at runtime
|
|
75
|
+
// ctx.dispose() Unsubscribe all event/state listeners (auto-called on destroy)
|
|
68
76
|
//
|
|
69
77
|
// ── Other exports ───────────────────────────────────────────────
|
|
70
78
|
//
|
|
@@ -82,13 +90,12 @@
|
|
|
82
90
|
//
|
|
83
91
|
// • Use lazy:true for heavy tabs — init runs only on first open
|
|
84
92
|
// • Build all DOM in init(); no index.html edits needed
|
|
85
|
-
// •
|
|
86
|
-
//
|
|
87
|
-
// • Use ctx.
|
|
88
|
-
//
|
|
89
|
-
// • Use ctx.
|
|
90
|
-
// •
|
|
91
|
-
// • See plugins/event-stream/client.js for a full working example
|
|
93
|
+
// • ctx.on/onState return unsubscribe fns; all auto-cleaned on tab destroy
|
|
94
|
+
// • onActivate(ctx), onDeactivate(ctx), onDestroy(ctx) all receive ctx
|
|
95
|
+
// • Use ctx.storage for persistent data (scoped to your plugin ID)
|
|
96
|
+
// • ALWAYS use ctx.getProjectPath() to read the current project path
|
|
97
|
+
// • Use ctx.on('projectChanged', fn) to reload data on project switch
|
|
98
|
+
// • See plugins/claude-editor/client.js for a full working example
|
|
92
99
|
//
|
|
93
100
|
// ── Project-aware plugin example ────────────────────────────────
|
|
94
101
|
//
|
|
@@ -115,16 +122,24 @@
|
|
|
115
122
|
// ════════════════════════════════════════════════════════════════
|
|
116
123
|
|
|
117
124
|
import { $ } from '../core/dom.js';
|
|
118
|
-
import { emit, on } from '../core/events.js';
|
|
119
|
-
import { getState, on as onState } from '../core/store.js';
|
|
125
|
+
import { emit, on, off } from '../core/events.js';
|
|
126
|
+
import { getState, on as onState, off as offState } from '../core/store.js';
|
|
120
127
|
import * as api from '../core/api.js';
|
|
121
128
|
import {
|
|
122
129
|
getAvailablePlugins, getEnabledPluginNames, setEnabledPluginNames,
|
|
123
130
|
getPluginMeta, loadPluginByName, isPluginLoaded,
|
|
124
131
|
trackPluginTab, getPluginTabId, getPluginTabMap,
|
|
125
132
|
setTabIdResolver, getSortedPlugins, setPluginOrder,
|
|
133
|
+
fetchMarketplace, installMarketplacePlugin, uninstallMarketplacePlugin,
|
|
126
134
|
} from '../core/plugin-loader.js';
|
|
127
135
|
|
|
136
|
+
/** Escape HTML to prevent XSS when rendering user-supplied plugin metadata */
|
|
137
|
+
function esc(str) {
|
|
138
|
+
const d = document.createElement('div');
|
|
139
|
+
d.textContent = str;
|
|
140
|
+
return d.innerHTML;
|
|
141
|
+
}
|
|
142
|
+
|
|
128
143
|
const registeredTabs = new Map();
|
|
129
144
|
const unregisteredConfigs = new Map(); // stores configs for re-registration
|
|
130
145
|
|
|
@@ -200,10 +215,14 @@ export function reRegisterTab(tabId) {
|
|
|
200
215
|
export function unregisterTab(id) {
|
|
201
216
|
const tab = registeredTabs.get(id);
|
|
202
217
|
if (!tab) return;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
218
|
+
try {
|
|
219
|
+
if (tab.onDestroy) tab.onDestroy(tab._ctx);
|
|
220
|
+
} finally {
|
|
221
|
+
if (tab._ctx) tab._ctx.dispose(); // auto-cleanup all event/state subscriptions
|
|
222
|
+
if (tab._btnEl) tab._btnEl.remove();
|
|
223
|
+
if (tab._paneEl) tab._paneEl.remove();
|
|
224
|
+
registeredTabs.delete(id);
|
|
225
|
+
}
|
|
207
226
|
}
|
|
208
227
|
|
|
209
228
|
/**
|
|
@@ -216,14 +235,29 @@ export function getRegisteredTabs() {
|
|
|
216
235
|
// ── Internal ────────────────────────────────────────────
|
|
217
236
|
|
|
218
237
|
function buildCtx(tab) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
238
|
+
// Track subscriptions for cleanup on destroy
|
|
239
|
+
const _unsubs = [];
|
|
240
|
+
|
|
241
|
+
const ctx = {
|
|
242
|
+
// Plugin identity
|
|
243
|
+
pluginId: tab.id,
|
|
244
|
+
|
|
245
|
+
// Event bus (returns unsubscribe handle)
|
|
246
|
+
on(event, fn) {
|
|
247
|
+
const unsub = on(event, fn);
|
|
248
|
+
_unsubs.push(unsub);
|
|
249
|
+
return unsub;
|
|
250
|
+
},
|
|
251
|
+
off,
|
|
222
252
|
emit,
|
|
223
253
|
|
|
224
|
-
// State
|
|
254
|
+
// State (returns unsubscribe handle)
|
|
225
255
|
getState,
|
|
226
|
-
onState,
|
|
256
|
+
onState(key, fn) {
|
|
257
|
+
const unsub = onState(key, fn);
|
|
258
|
+
_unsubs.push(unsub);
|
|
259
|
+
return unsub;
|
|
260
|
+
},
|
|
227
261
|
|
|
228
262
|
// API
|
|
229
263
|
api,
|
|
@@ -232,6 +266,43 @@ function buildCtx(tab) {
|
|
|
232
266
|
getProjectPath: () => $.projectSelect?.value || '',
|
|
233
267
|
getSessionId: () => getState('sessionId'),
|
|
234
268
|
|
|
269
|
+
// Theme
|
|
270
|
+
getTheme: () => document.documentElement.getAttribute('data-theme') || 'dark',
|
|
271
|
+
|
|
272
|
+
// Namespaced localStorage
|
|
273
|
+
storage: {
|
|
274
|
+
get(key) {
|
|
275
|
+
try { return JSON.parse(localStorage.getItem(`claudeck-plugin-${tab.id}-${key}`)); }
|
|
276
|
+
catch { return null; }
|
|
277
|
+
},
|
|
278
|
+
set(key, value) {
|
|
279
|
+
localStorage.setItem(`claudeck-plugin-${tab.id}-${key}`, JSON.stringify(value));
|
|
280
|
+
},
|
|
281
|
+
remove(key) {
|
|
282
|
+
localStorage.removeItem(`claudeck-plugin-${tab.id}-${key}`);
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
// Toast notifications
|
|
287
|
+
toast(message, opts = {}) {
|
|
288
|
+
const { duration = 3000, type = 'info' } = opts;
|
|
289
|
+
const el = document.createElement('div');
|
|
290
|
+
el.className = `claudeck-toast claudeck-toast-${type}`;
|
|
291
|
+
el.textContent = message;
|
|
292
|
+
el.style.cssText = `
|
|
293
|
+
position:fixed;bottom:24px;right:24px;z-index:99999;
|
|
294
|
+
padding:10px 20px;border-radius:8px;font-size:13px;
|
|
295
|
+
font-family:var(--font-sans);color:#fff;pointer-events:auto;
|
|
296
|
+
animation:claudeck-toast-in .3s ease;
|
|
297
|
+
background:${type === 'error' ? 'var(--error,#e54)' : type === 'success' ? 'var(--success,#33d17a)' : 'var(--bg-elevated,#333)'};
|
|
298
|
+
border:1px solid ${type === 'error' ? 'var(--error,#e54)' : type === 'success' ? 'var(--success,#33d17a)' : 'var(--border,#444)'};
|
|
299
|
+
box-shadow:var(--shadow-md,0 4px 12px rgba(0,0,0,.3));
|
|
300
|
+
`;
|
|
301
|
+
document.body.appendChild(el);
|
|
302
|
+
setTimeout(() => { el.style.opacity = '0'; el.style.transition = 'opacity .3s'; }, duration - 300);
|
|
303
|
+
setTimeout(() => el.remove(), duration);
|
|
304
|
+
},
|
|
305
|
+
|
|
235
306
|
// Tab-specific
|
|
236
307
|
showBadge(count) {
|
|
237
308
|
if (!tab._btnEl) return;
|
|
@@ -261,7 +332,15 @@ function buildCtx(tab) {
|
|
|
261
332
|
else tab._btnEl.childNodes[tab._btnEl.childNodes.length - 1].textContent = text;
|
|
262
333
|
}
|
|
263
334
|
},
|
|
335
|
+
|
|
336
|
+
/** Unsubscribe all event/state listeners registered via this ctx */
|
|
337
|
+
dispose() {
|
|
338
|
+
_unsubs.forEach(fn => fn());
|
|
339
|
+
_unsubs.length = 0;
|
|
340
|
+
},
|
|
264
341
|
};
|
|
342
|
+
|
|
343
|
+
return ctx;
|
|
265
344
|
}
|
|
266
345
|
|
|
267
346
|
function mountTab(tab) {
|
|
@@ -339,6 +418,7 @@ function initTabContent(tab) {
|
|
|
339
418
|
tab._initialized = true;
|
|
340
419
|
|
|
341
420
|
const ctx = buildCtx(tab);
|
|
421
|
+
tab._ctx = ctx; // store for lifecycle hooks and cleanup
|
|
342
422
|
const el = tab.init(ctx);
|
|
343
423
|
if (el instanceof HTMLElement) {
|
|
344
424
|
tab._paneEl.appendChild(el);
|
|
@@ -357,9 +437,9 @@ function onTabActivated(tabId) {
|
|
|
357
437
|
for (const [id, tab] of registeredTabs) {
|
|
358
438
|
if (id === tabId) {
|
|
359
439
|
ensureInit(tab);
|
|
360
|
-
if (tab.onActivate) tab.onActivate();
|
|
440
|
+
if (tab.onActivate) tab.onActivate(tab._ctx);
|
|
361
441
|
} else {
|
|
362
|
-
if (tab._initialized && tab.onDeactivate) tab.onDeactivate();
|
|
442
|
+
if (tab._initialized && tab.onDeactivate) tab.onDeactivate(tab._ctx);
|
|
363
443
|
}
|
|
364
444
|
}
|
|
365
445
|
}
|
|
@@ -415,9 +495,6 @@ function openMarketplace() {
|
|
|
415
495
|
// Don't open multiple
|
|
416
496
|
if (document.querySelector('.marketplace-overlay')) return;
|
|
417
497
|
|
|
418
|
-
const plugins = getSortedPlugins();
|
|
419
|
-
const enabled = new Set(getEnabledPluginNames());
|
|
420
|
-
|
|
421
498
|
// Overlay
|
|
422
499
|
const overlay = document.createElement('div');
|
|
423
500
|
overlay.className = 'marketplace-overlay';
|
|
@@ -426,197 +503,339 @@ function openMarketplace() {
|
|
|
426
503
|
const popup = document.createElement('div');
|
|
427
504
|
popup.className = 'marketplace-popup';
|
|
428
505
|
|
|
429
|
-
// Header
|
|
506
|
+
// Header with tabs
|
|
430
507
|
const header = document.createElement('div');
|
|
431
508
|
header.className = 'marketplace-header';
|
|
432
509
|
header.innerHTML = `
|
|
433
510
|
<h3>Plugin Marketplace</h3>
|
|
434
|
-
<
|
|
511
|
+
<div class="marketplace-tabs">
|
|
512
|
+
<button class="marketplace-tab active" data-marketplace-tab="installed">Installed</button>
|
|
513
|
+
<button class="marketplace-tab" data-marketplace-tab="community">Community</button>
|
|
514
|
+
</div>
|
|
435
515
|
`;
|
|
436
516
|
popup.appendChild(header);
|
|
437
517
|
|
|
438
|
-
//
|
|
439
|
-
const
|
|
440
|
-
|
|
518
|
+
// Tab content container
|
|
519
|
+
const tabContent = document.createElement('div');
|
|
520
|
+
tabContent.className = 'marketplace-tab-content';
|
|
521
|
+
popup.appendChild(tabContent);
|
|
441
522
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
523
|
+
// Footer (shared by both tabs)
|
|
524
|
+
const footer = document.createElement('div');
|
|
525
|
+
footer.className = 'marketplace-footer';
|
|
526
|
+
popup.appendChild(footer);
|
|
445
527
|
|
|
446
|
-
//
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
const loaded = tabId && registeredTabs.has(tabId);
|
|
457
|
-
|
|
458
|
-
const item = document.createElement('div');
|
|
459
|
-
item.className = 'marketplace-item';
|
|
460
|
-
item.dataset.plugin = plugin.name;
|
|
461
|
-
item.draggable = true;
|
|
462
|
-
if (pending.has(plugin.name)) item.classList.add('selected');
|
|
463
|
-
|
|
464
|
-
item.innerHTML = `
|
|
465
|
-
<div class="marketplace-drag-handle" title="Drag to reorder">⠿</div>
|
|
466
|
-
<div class="marketplace-item-icon">${meta.icon}</div>
|
|
467
|
-
<div class="marketplace-item-info">
|
|
468
|
-
<div class="marketplace-item-name">${formatPluginName(plugin.name)}</div>
|
|
469
|
-
<div class="marketplace-item-desc">${meta.description}</div>
|
|
470
|
-
</div>
|
|
471
|
-
<div class="marketplace-item-status">
|
|
472
|
-
${loaded ? '<span class="marketplace-loaded">loaded</span>' : ''}
|
|
473
|
-
</div>
|
|
474
|
-
<div class="marketplace-item-toggle">
|
|
475
|
-
<div class="marketplace-checkbox ${pending.has(plugin.name) ? 'checked' : ''}"></div>
|
|
476
|
-
</div>
|
|
477
|
-
`;
|
|
478
|
-
|
|
479
|
-
// Toggle selection (ignore clicks on drag handle)
|
|
480
|
-
item.addEventListener('click', (e) => {
|
|
481
|
-
if (e.target.closest('.marketplace-drag-handle')) return;
|
|
482
|
-
const cb = item.querySelector('.marketplace-checkbox');
|
|
483
|
-
if (pending.has(plugin.name)) {
|
|
484
|
-
pending.delete(plugin.name);
|
|
485
|
-
cb.classList.remove('checked');
|
|
486
|
-
item.classList.remove('selected');
|
|
487
|
-
} else {
|
|
488
|
-
pending.add(plugin.name);
|
|
489
|
-
cb.classList.add('checked');
|
|
490
|
-
item.classList.add('selected');
|
|
491
|
-
}
|
|
528
|
+
// ── Tab switching ──
|
|
529
|
+
let activeTab = 'installed';
|
|
530
|
+
const tabBtns = header.querySelectorAll('.marketplace-tab');
|
|
531
|
+
tabBtns.forEach(btn => {
|
|
532
|
+
btn.addEventListener('click', () => {
|
|
533
|
+
tabBtns.forEach(b => b.classList.remove('active'));
|
|
534
|
+
btn.classList.add('active');
|
|
535
|
+
activeTab = btn.dataset.marketplaceTab;
|
|
536
|
+
if (activeTab === 'installed') renderInstalledTab();
|
|
537
|
+
else renderCommunityTab();
|
|
492
538
|
});
|
|
539
|
+
});
|
|
493
540
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
541
|
+
// ── Installed tab ──
|
|
542
|
+
function renderInstalledTab() {
|
|
543
|
+
const plugins = getSortedPlugins();
|
|
544
|
+
const enabled = new Set(getEnabledPluginNames());
|
|
545
|
+
const pending = new Set(enabled);
|
|
499
546
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
dragPlaceholder.className = 'marketplace-drop-indicator';
|
|
547
|
+
tabContent.innerHTML = '';
|
|
548
|
+
footer.innerHTML = '';
|
|
503
549
|
|
|
504
|
-
|
|
505
|
-
|
|
550
|
+
const subtitle = document.createElement('div');
|
|
551
|
+
subtitle.className = 'marketplace-subtitle';
|
|
552
|
+
subtitle.textContent = `${plugins.length} plugin${plugins.length !== 1 ? 's' : ''} available · drag to reorder`;
|
|
553
|
+
tabContent.appendChild(subtitle);
|
|
554
|
+
|
|
555
|
+
const list = document.createElement('div');
|
|
556
|
+
list.className = 'marketplace-list';
|
|
557
|
+
|
|
558
|
+
if (!plugins.length) {
|
|
559
|
+
list.innerHTML = '<div class="marketplace-empty">No plugins available.<br>Drop files into <code>plugins/</code> to get started.</div>';
|
|
560
|
+
}
|
|
506
561
|
|
|
507
|
-
|
|
508
|
-
|
|
562
|
+
let dragItem = null;
|
|
563
|
+
let dragPlaceholder = null;
|
|
564
|
+
|
|
565
|
+
for (const plugin of plugins) {
|
|
566
|
+
const meta = getPluginMeta(plugin.name);
|
|
567
|
+
const tabId = getPluginTabId(plugin.name);
|
|
568
|
+
const loaded = tabId && registeredTabs.has(tabId);
|
|
569
|
+
|
|
570
|
+
const item = document.createElement('div');
|
|
571
|
+
item.className = 'marketplace-item';
|
|
572
|
+
item.dataset.plugin = plugin.name;
|
|
573
|
+
item.draggable = true;
|
|
574
|
+
if (pending.has(plugin.name)) item.classList.add('selected');
|
|
575
|
+
|
|
576
|
+
const sourceLabel = plugin.fromMarketplace ? '<span class="marketplace-source community">community</span>' : '';
|
|
577
|
+
|
|
578
|
+
item.innerHTML = `
|
|
579
|
+
<div class="marketplace-drag-handle" title="Drag to reorder">⠿</div>
|
|
580
|
+
<div class="marketplace-item-icon">${esc(meta.icon || '🧩')}</div>
|
|
581
|
+
<div class="marketplace-item-info">
|
|
582
|
+
<div class="marketplace-item-name">${esc(formatPluginName(plugin.name))} ${sourceLabel}</div>
|
|
583
|
+
<div class="marketplace-item-desc">${esc(meta.description || '')}</div>
|
|
584
|
+
</div>
|
|
585
|
+
<div class="marketplace-item-status">
|
|
586
|
+
${loaded ? '<span class="marketplace-loaded">loaded</span>' : ''}
|
|
587
|
+
</div>
|
|
588
|
+
<div class="marketplace-item-toggle">
|
|
589
|
+
<div class="marketplace-checkbox ${pending.has(plugin.name) ? 'checked' : ''}"></div>
|
|
590
|
+
</div>
|
|
591
|
+
`;
|
|
592
|
+
|
|
593
|
+
item.addEventListener('click', (e) => {
|
|
594
|
+
if (e.target.closest('.marketplace-drag-handle')) return;
|
|
595
|
+
const cb = item.querySelector('.marketplace-checkbox');
|
|
596
|
+
if (pending.has(plugin.name)) {
|
|
597
|
+
pending.delete(plugin.name);
|
|
598
|
+
cb.classList.remove('checked');
|
|
599
|
+
item.classList.remove('selected');
|
|
600
|
+
} else {
|
|
601
|
+
pending.add(plugin.name);
|
|
602
|
+
cb.classList.add('checked');
|
|
603
|
+
item.classList.add('selected');
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
// Drag events
|
|
608
|
+
item.addEventListener('dragstart', (e) => {
|
|
609
|
+
dragItem = item;
|
|
610
|
+
item.classList.add('dragging');
|
|
611
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
612
|
+
dragPlaceholder = document.createElement('div');
|
|
613
|
+
dragPlaceholder.className = 'marketplace-drop-indicator';
|
|
614
|
+
requestAnimationFrame(() => { item.style.opacity = '0.4'; });
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
item.addEventListener('dragend', () => {
|
|
618
|
+
if (dragItem) { dragItem.classList.remove('dragging'); dragItem.style.opacity = ''; }
|
|
619
|
+
if (dragPlaceholder?.parentNode) dragPlaceholder.remove();
|
|
620
|
+
dragItem = null;
|
|
621
|
+
dragPlaceholder = null;
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
item.addEventListener('dragover', (e) => {
|
|
625
|
+
e.preventDefault();
|
|
626
|
+
e.dataTransfer.dropEffect = 'move';
|
|
627
|
+
if (!dragItem || dragItem === item) return;
|
|
628
|
+
const rect = item.getBoundingClientRect();
|
|
629
|
+
const after = e.clientY > rect.top + rect.height / 2;
|
|
630
|
+
if (after) item.after(dragPlaceholder);
|
|
631
|
+
else item.before(dragPlaceholder);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
item.addEventListener('drop', (e) => {
|
|
635
|
+
e.preventDefault();
|
|
636
|
+
if (!dragItem || dragItem === item) return;
|
|
637
|
+
if (dragPlaceholder?.parentNode) {
|
|
638
|
+
dragPlaceholder.before(dragItem);
|
|
639
|
+
dragPlaceholder.remove();
|
|
640
|
+
}
|
|
509
641
|
dragItem.classList.remove('dragging');
|
|
510
642
|
dragItem.style.opacity = '';
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
}
|
|
515
|
-
dragItem = null;
|
|
516
|
-
dragPlaceholder = null;
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
item.addEventListener('dragover', (e) => {
|
|
520
|
-
e.preventDefault();
|
|
521
|
-
e.dataTransfer.dropEffect = 'move';
|
|
522
|
-
if (!dragItem || dragItem === item) return;
|
|
643
|
+
dragItem = null;
|
|
644
|
+
dragPlaceholder = null;
|
|
645
|
+
});
|
|
523
646
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const after = e.clientY > midY;
|
|
647
|
+
list.appendChild(item);
|
|
648
|
+
}
|
|
527
649
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
650
|
+
tabContent.appendChild(list);
|
|
651
|
+
|
|
652
|
+
// Footer buttons
|
|
653
|
+
const cancelBtn = document.createElement('button');
|
|
654
|
+
cancelBtn.className = 'marketplace-btn marketplace-btn-cancel';
|
|
655
|
+
cancelBtn.textContent = 'Cancel';
|
|
656
|
+
cancelBtn.addEventListener('click', () => closeMarketplace());
|
|
657
|
+
|
|
658
|
+
const applyBtn = document.createElement('button');
|
|
659
|
+
applyBtn.className = 'marketplace-btn marketplace-btn-apply';
|
|
660
|
+
applyBtn.textContent = 'Apply';
|
|
661
|
+
applyBtn.addEventListener('click', async () => {
|
|
662
|
+
const orderedNames = [...list.querySelectorAll('.marketplace-item')]
|
|
663
|
+
.map(el => el.dataset.plugin).filter(Boolean);
|
|
664
|
+
setPluginOrder(orderedNames);
|
|
665
|
+
const newEnabled = orderedNames.filter(n => pending.has(n));
|
|
666
|
+
setEnabledPluginNames(newEnabled);
|
|
667
|
+
|
|
668
|
+
for (const [id] of [...registeredTabs]) {
|
|
669
|
+
if (!isPluginTab(id)) continue;
|
|
670
|
+
if (!newEnabled.some(n => getPluginTabId(n) === id)) unregisterTab(id);
|
|
532
671
|
}
|
|
533
|
-
});
|
|
534
672
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
if (dragPlaceholder && dragPlaceholder.parentNode) {
|
|
541
|
-
dragPlaceholder.before(dragItem);
|
|
542
|
-
dragPlaceholder.remove();
|
|
673
|
+
for (const name of newEnabled) {
|
|
674
|
+
const existingTabId = getPluginTabId(name);
|
|
675
|
+
if (existingTabId && registeredTabs.has(existingTabId)) continue;
|
|
676
|
+
if (existingTabId && reRegisterTab(existingTabId)) continue;
|
|
677
|
+
if (!isPluginLoaded(name)) await loadPluginByName(name);
|
|
543
678
|
}
|
|
544
679
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
dragItem = null;
|
|
548
|
-
dragPlaceholder = null;
|
|
680
|
+
reorderPluginTabs(newEnabled);
|
|
681
|
+
closeMarketplace();
|
|
549
682
|
});
|
|
550
683
|
|
|
551
|
-
|
|
684
|
+
footer.appendChild(cancelBtn);
|
|
685
|
+
footer.appendChild(applyBtn);
|
|
552
686
|
}
|
|
553
687
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
688
|
+
// ── Community tab ──
|
|
689
|
+
async function renderCommunityTab() {
|
|
690
|
+
const requestedTab = activeTab;
|
|
691
|
+
tabContent.innerHTML = '';
|
|
692
|
+
footer.innerHTML = '';
|
|
693
|
+
|
|
694
|
+
// Loading state
|
|
695
|
+
const loading = document.createElement('div');
|
|
696
|
+
loading.className = 'marketplace-loading';
|
|
697
|
+
loading.innerHTML = '<div class="marketplace-spinner"></div><span>Loading community plugins...</span>';
|
|
698
|
+
tabContent.appendChild(loading);
|
|
699
|
+
|
|
700
|
+
// Close button in footer
|
|
701
|
+
const closeBtn = document.createElement('button');
|
|
702
|
+
closeBtn.className = 'marketplace-btn marketplace-btn-cancel';
|
|
703
|
+
closeBtn.textContent = 'Close';
|
|
704
|
+
closeBtn.addEventListener('click', () => closeMarketplace());
|
|
705
|
+
footer.appendChild(closeBtn);
|
|
706
|
+
|
|
707
|
+
const registry = await fetchMarketplace();
|
|
708
|
+
if (!overlay.isConnected || activeTab !== requestedTab) return;
|
|
709
|
+
tabContent.innerHTML = '';
|
|
710
|
+
|
|
711
|
+
if (!registry || !registry.plugins?.length) {
|
|
712
|
+
tabContent.innerHTML = `
|
|
713
|
+
<div class="marketplace-empty">
|
|
714
|
+
No community plugins available yet.<br>
|
|
715
|
+
<a href="https://github.com/hamedafarag/claudeck-marketplace" target="_blank" rel="noopener">
|
|
716
|
+
Submit your plugin →
|
|
717
|
+
</a>
|
|
718
|
+
</div>
|
|
719
|
+
`;
|
|
720
|
+
return;
|
|
587
721
|
}
|
|
588
722
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
723
|
+
const subtitle = document.createElement('div');
|
|
724
|
+
subtitle.className = 'marketplace-subtitle';
|
|
725
|
+
subtitle.textContent = `${registry.plugins.length} community plugin${registry.plugins.length !== 1 ? 's' : ''} available`;
|
|
726
|
+
tabContent.appendChild(subtitle);
|
|
727
|
+
|
|
728
|
+
const list = document.createElement('div');
|
|
729
|
+
list.className = 'marketplace-list';
|
|
730
|
+
|
|
731
|
+
for (const plugin of registry.plugins) {
|
|
732
|
+
const item = document.createElement('div');
|
|
733
|
+
item.className = 'marketplace-item marketplace-community-item';
|
|
734
|
+
item.dataset.plugin = plugin.id;
|
|
735
|
+
|
|
736
|
+
const icon = plugin.icon || '🧩';
|
|
737
|
+
const hasServer = plugin.hasServer;
|
|
738
|
+
const serverBadge = hasServer ? '<span class="marketplace-source server" title="This plugin includes server-side code">server</span>' : '';
|
|
739
|
+
|
|
740
|
+
let actionHtml;
|
|
741
|
+
if (plugin.isBuiltin) {
|
|
742
|
+
actionHtml = `<span class="marketplace-action-btn" style="opacity:.5;cursor:default;border:none;">Built-in</span>`;
|
|
743
|
+
} else if (plugin.updateAvailable) {
|
|
744
|
+
actionHtml = `<button class="marketplace-action-btn marketplace-update-btn" data-action="update">Update</button>`;
|
|
745
|
+
} else if (plugin.installed) {
|
|
746
|
+
actionHtml = `<button class="marketplace-action-btn marketplace-uninstall-btn" data-action="uninstall">Uninstall</button>`;
|
|
747
|
+
} else {
|
|
748
|
+
actionHtml = `<button class="marketplace-action-btn marketplace-install-btn" data-action="install">Install</button>`;
|
|
749
|
+
}
|
|
594
750
|
|
|
595
|
-
|
|
751
|
+
item.innerHTML = `
|
|
752
|
+
<div class="marketplace-item-icon">${esc(icon)}</div>
|
|
753
|
+
<div class="marketplace-item-info">
|
|
754
|
+
<div class="marketplace-item-name">
|
|
755
|
+
${esc(plugin.name || formatPluginName(plugin.id))} ${serverBadge}
|
|
756
|
+
</div>
|
|
757
|
+
<div class="marketplace-item-desc">${esc(plugin.description || '')}</div>
|
|
758
|
+
<div class="marketplace-item-meta">
|
|
759
|
+
<span class="marketplace-author">by ${esc(plugin.author || 'unknown')}</span>
|
|
760
|
+
<span class="marketplace-version">v${esc(plugin.version || '0.0.0')}</span>
|
|
761
|
+
${plugin.installedVersion && plugin.updateAvailable ? `<span class="marketplace-version-old">installed: v${esc(plugin.installedVersion)}</span>` : ''}
|
|
762
|
+
</div>
|
|
763
|
+
</div>
|
|
764
|
+
<div class="marketplace-item-actions">${actionHtml}</div>
|
|
765
|
+
`;
|
|
766
|
+
|
|
767
|
+
// Action button handler (skip built-in plugins which have no interactive action)
|
|
768
|
+
const actionBtn = item.querySelector('.marketplace-action-btn');
|
|
769
|
+
if (!plugin.isBuiltin) actionBtn.addEventListener('click', async (e) => {
|
|
770
|
+
e.stopPropagation();
|
|
771
|
+
const action = actionBtn.dataset.action;
|
|
772
|
+
actionBtn.disabled = true;
|
|
773
|
+
actionBtn.textContent = action === 'uninstall' ? 'Removing...' : 'Installing...';
|
|
774
|
+
|
|
775
|
+
try {
|
|
776
|
+
if (action === 'uninstall') {
|
|
777
|
+
// Resolve tab ID before uninstall clears plugin metadata
|
|
778
|
+
const tabId = getPluginTabId(plugin.id);
|
|
779
|
+
await uninstallMarketplacePlugin(plugin.id);
|
|
780
|
+
if (tabId) unregisterTab(tabId);
|
|
781
|
+
plugin.installed = false;
|
|
782
|
+
plugin.installedVersion = null;
|
|
783
|
+
plugin.updateAvailable = false;
|
|
784
|
+
actionBtn.textContent = 'Install';
|
|
785
|
+
actionBtn.className = 'marketplace-action-btn marketplace-install-btn';
|
|
786
|
+
actionBtn.dataset.action = 'install';
|
|
787
|
+
} else {
|
|
788
|
+
// Install or update
|
|
789
|
+
if (hasServer) {
|
|
790
|
+
const pluginLabel = plugin.name || plugin.id;
|
|
791
|
+
const proceed = confirm(
|
|
792
|
+
`"${pluginLabel}" includes server-side code that will run on your machine.\n\nServer plugins require CLAUDECK_USER_SERVER_PLUGINS=true to enable server routes.\n\nContinue with installation?`
|
|
793
|
+
);
|
|
794
|
+
if (!proceed) {
|
|
795
|
+
actionBtn.disabled = false;
|
|
796
|
+
actionBtn.textContent = action === 'update' ? 'Update' : 'Install';
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
await installMarketplacePlugin(plugin);
|
|
801
|
+
plugin.installed = true;
|
|
802
|
+
plugin.installedVersion = plugin.version;
|
|
803
|
+
plugin.updateAvailable = false;
|
|
804
|
+
actionBtn.textContent = 'Uninstall';
|
|
805
|
+
actionBtn.className = 'marketplace-action-btn marketplace-uninstall-btn';
|
|
806
|
+
actionBtn.dataset.action = 'uninstall';
|
|
807
|
+
}
|
|
808
|
+
} catch (err) {
|
|
809
|
+
actionBtn.textContent = 'Error';
|
|
810
|
+
console.error(`Marketplace ${action} failed:`, err);
|
|
811
|
+
setTimeout(() => {
|
|
812
|
+
actionBtn.textContent = action === 'uninstall' ? 'Uninstall' : (action === 'update' ? 'Update' : 'Install');
|
|
813
|
+
}, 2000);
|
|
814
|
+
}
|
|
815
|
+
actionBtn.disabled = false;
|
|
816
|
+
});
|
|
596
817
|
|
|
597
|
-
|
|
598
|
-
await loadPluginByName(name);
|
|
599
|
-
}
|
|
818
|
+
list.appendChild(item);
|
|
600
819
|
}
|
|
601
820
|
|
|
602
|
-
|
|
603
|
-
|
|
821
|
+
tabContent.appendChild(list);
|
|
822
|
+
}
|
|
604
823
|
|
|
824
|
+
const closeMarketplace = () => {
|
|
825
|
+
document.removeEventListener('keydown', onKey);
|
|
605
826
|
overlay.remove();
|
|
606
|
-
}
|
|
827
|
+
};
|
|
607
828
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
popup.appendChild(footer);
|
|
829
|
+
// Initial render
|
|
830
|
+
renderInstalledTab();
|
|
611
831
|
|
|
612
832
|
overlay.appendChild(popup);
|
|
613
833
|
overlay.addEventListener('click', (e) => {
|
|
614
|
-
if (e.target === overlay)
|
|
834
|
+
if (e.target === overlay) closeMarketplace();
|
|
615
835
|
});
|
|
616
836
|
|
|
617
|
-
// Close on Escape
|
|
618
837
|
const onKey = (e) => {
|
|
619
|
-
if (e.key === 'Escape')
|
|
838
|
+
if (e.key === 'Escape') closeMarketplace();
|
|
620
839
|
};
|
|
621
840
|
document.addEventListener('keydown', onKey);
|
|
622
841
|
|