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.
Files changed (61) hide show
  1. package/README.md +6 -8
  2. package/package.json +1 -1
  3. package/plugins/claude-editor/manifest.json +10 -0
  4. package/plugins/repos/manifest.json +10 -0
  5. package/public/css/core/theme.css +6 -21
  6. package/public/css/core/variables.css +2 -0
  7. package/public/css/features/message-queue.css +348 -0
  8. package/public/css/ui/commands.css +4 -4
  9. package/public/css/ui/messages.css +310 -78
  10. package/public/css/ui/right-panel.css +207 -0
  11. package/public/css/ui/sessions.css +173 -0
  12. package/public/css/ui/settings.css +75 -0
  13. package/public/index.html +10 -2
  14. package/public/js/components/add-project-modal.js +14 -0
  15. package/public/js/components/jump-to-latest.js +42 -0
  16. package/public/js/components/queue-stop-modal.js +23 -0
  17. package/public/js/components/settings-modal.js +65 -0
  18. package/public/js/core/api.js +15 -43
  19. package/public/js/core/dom.js +17 -0
  20. package/public/js/core/events.js +11 -0
  21. package/public/js/core/plugin-loader.js +96 -11
  22. package/public/js/core/store.js +11 -0
  23. package/public/js/core/utils.js +38 -2
  24. package/public/js/features/chat.js +49 -1
  25. package/public/js/features/message-queue.js +423 -0
  26. package/public/js/features/projects.js +185 -3
  27. package/public/js/main.js +4 -1
  28. package/public/js/panels/assistant-bot.js +16 -0
  29. package/public/js/panels/dev-docs.js +2 -2
  30. package/public/js/panels/memory.js +1 -0
  31. package/public/js/ui/context-gauge.js +10 -1
  32. package/public/js/ui/formatting.js +65 -11
  33. package/public/js/ui/header-dropdowns.js +30 -0
  34. package/public/js/ui/input-meta.js +13 -6
  35. package/public/js/ui/max-turns.js +6 -3
  36. package/public/js/ui/messages.js +97 -1
  37. package/public/js/ui/model-selector.js +1 -0
  38. package/public/js/ui/parallel.js +32 -2
  39. package/public/js/ui/permissions.js +1 -0
  40. package/public/js/ui/right-panel.js +0 -8
  41. package/public/js/ui/tab-sdk.js +395 -176
  42. package/public/style.css +2 -0
  43. package/server/memory-optimizer.js +17 -13
  44. package/server/routes/marketplace.js +316 -0
  45. package/server/routes/projects.js +0 -0
  46. package/server/ws-handler.js +22 -15
  47. package/server.js +18 -0
  48. package/plugins/event-stream/client.css +0 -207
  49. package/plugins/event-stream/client.js +0 -271
  50. package/plugins/linear/client.css +0 -345
  51. package/plugins/linear/client.js +0 -380
  52. package/plugins/linear/config.json +0 -5
  53. package/plugins/linear/server.js +0 -312
  54. package/plugins/sudoku/client.css +0 -196
  55. package/plugins/sudoku/client.js +0 -329
  56. package/plugins/tasks/client.css +0 -414
  57. package/plugins/tasks/client.js +0 -394
  58. package/plugins/tasks/server.js +0 -116
  59. package/plugins/tic-tac-toe/client.css +0 -167
  60. package/plugins/tic-tac-toe/client.js +0 -241
  61. package/public/js/components/linear-create-modal.js +0 -43
@@ -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.on(event, fn) Subscribe to the app event bus
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
- // • ALWAYS use ctx.getProjectPath() to read the current project path.
86
- // NEVER use document.getElementById('project-select') directly.
87
- // • Use ctx.on('projectChanged', fn) to reload data on project switch.
88
- // NEVER add your own change listener to the project select element.
89
- // • Use ctx.onState('sessionId', fn) to reload on session switch
90
- // • Existing shortcuts (e.g. openRightPanel('my-tab')) work automatically
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
- if (tab.onDestroy) tab.onDestroy();
204
- if (tab._btnEl) tab._btnEl.remove();
205
- if (tab._paneEl) tab._paneEl.remove();
206
- registeredTabs.delete(id);
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
- return {
220
- // Event bus
221
- on,
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
- <span class="marketplace-subtitle">${plugins.length} plugin${plugins.length !== 1 ? 's' : ''} available · drag to reorder</span>
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
- // Plugin list
439
- const list = document.createElement('div');
440
- list.className = 'marketplace-list';
518
+ // Tab content container
519
+ const tabContent = document.createElement('div');
520
+ tabContent.className = 'marketplace-tab-content';
521
+ popup.appendChild(tabContent);
441
522
 
442
- if (!plugins.length) {
443
- list.innerHTML = '<div class="marketplace-empty">No plugins available.<br>Drop files into <code>plugins/</code> to get started.</div>';
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
- // Track pending selections (start from current state)
447
- const pending = new Set(enabled);
448
-
449
- // ── Drag state ──
450
- let dragItem = null;
451
- let dragPlaceholder = null;
452
-
453
- for (const plugin of plugins) {
454
- const meta = getPluginMeta(plugin.name);
455
- const tabId = getPluginTabId(plugin.name);
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
- // ── Drag events ──
495
- item.addEventListener('dragstart', (e) => {
496
- dragItem = item;
497
- item.classList.add('dragging');
498
- e.dataTransfer.effectAllowed = 'move';
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
- // Create placeholder
501
- dragPlaceholder = document.createElement('div');
502
- dragPlaceholder.className = 'marketplace-drop-indicator';
547
+ tabContent.innerHTML = '';
548
+ footer.innerHTML = '';
503
549
 
504
- requestAnimationFrame(() => { item.style.opacity = '0.4'; });
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
- item.addEventListener('dragend', () => {
508
- if (dragItem) {
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
- if (dragPlaceholder && dragPlaceholder.parentNode) {
513
- dragPlaceholder.remove();
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
- const rect = item.getBoundingClientRect();
525
- const midY = rect.top + rect.height / 2;
526
- const after = e.clientY > midY;
647
+ list.appendChild(item);
648
+ }
527
649
 
528
- if (after) {
529
- item.after(dragPlaceholder);
530
- } else {
531
- item.before(dragPlaceholder);
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
- item.addEventListener('drop', (e) => {
536
- e.preventDefault();
537
- if (!dragItem || dragItem === item) return;
538
-
539
- // Insert dragged item where the placeholder is
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
- dragItem.classList.remove('dragging');
546
- dragItem.style.opacity = '';
547
- dragItem = null;
548
- dragPlaceholder = null;
680
+ reorderPluginTabs(newEnabled);
681
+ closeMarketplace();
549
682
  });
550
683
 
551
- list.appendChild(item);
684
+ footer.appendChild(cancelBtn);
685
+ footer.appendChild(applyBtn);
552
686
  }
553
687
 
554
- popup.appendChild(list);
555
-
556
- // Footer with Apply / Cancel
557
- const footer = document.createElement('div');
558
- footer.className = 'marketplace-footer';
559
-
560
- const cancelBtn = document.createElement('button');
561
- cancelBtn.className = 'marketplace-btn marketplace-btn-cancel';
562
- cancelBtn.textContent = 'Cancel';
563
- cancelBtn.addEventListener('click', () => overlay.remove());
564
-
565
- const applyBtn = document.createElement('button');
566
- applyBtn.className = 'marketplace-btn marketplace-btn-apply';
567
- applyBtn.textContent = 'Apply';
568
- applyBtn.addEventListener('click', async () => {
569
- // Read order from current DOM positions
570
- const orderedNames = [...list.querySelectorAll('.marketplace-item')]
571
- .map(el => el.dataset.plugin)
572
- .filter(Boolean);
573
-
574
- setPluginOrder(orderedNames);
575
-
576
- // Only enabled in the order they appear
577
- const newEnabled = orderedNames.filter(n => pending.has(n));
578
- setEnabledPluginNames(newEnabled);
579
-
580
- // Unload (hide) disabled plugins first
581
- for (const [id] of [...registeredTabs]) {
582
- if (!isPluginTab(id)) continue;
583
- const belongsToAny = newEnabled.some(n => getPluginTabId(n) === id);
584
- if (!belongsToAny) {
585
- unregisterTab(id);
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
- // Load newly enabled plugins in order
590
- for (const name of newEnabled) {
591
- const existingTabId = getPluginTabId(name);
592
-
593
- if (existingTabId && registeredTabs.has(existingTabId)) continue;
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
- if (existingTabId && reRegisterTab(existingTabId)) continue;
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
- if (!isPluginLoaded(name)) {
598
- await loadPluginByName(name);
599
- }
818
+ list.appendChild(item);
600
819
  }
601
820
 
602
- // Reorder tab buttons & panes in the DOM to match marketplace order
603
- reorderPluginTabs(newEnabled);
821
+ tabContent.appendChild(list);
822
+ }
604
823
 
824
+ const closeMarketplace = () => {
825
+ document.removeEventListener('keydown', onKey);
605
826
  overlay.remove();
606
- });
827
+ };
607
828
 
608
- footer.appendChild(cancelBtn);
609
- footer.appendChild(applyBtn);
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) overlay.remove();
834
+ if (e.target === overlay) closeMarketplace();
615
835
  });
616
836
 
617
- // Close on Escape
618
837
  const onKey = (e) => {
619
- if (e.key === 'Escape') { overlay.remove(); document.removeEventListener('keydown', onKey); }
838
+ if (e.key === 'Escape') closeMarketplace();
620
839
  };
621
840
  document.addEventListener('keydown', onKey);
622
841