claudeck 1.4.0 → 1.4.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 (37) hide show
  1. package/README.md +6 -6
  2. package/package.json +1 -1
  3. package/plugins/claude-editor/manifest.json +10 -0
  4. package/plugins/linear/manifest.json +10 -0
  5. package/plugins/repos/manifest.json +10 -0
  6. package/public/css/ui/right-panel.css +207 -0
  7. package/public/css/ui/settings.css +75 -0
  8. package/public/index.html +7 -0
  9. package/public/js/components/settings-modal.js +65 -0
  10. package/public/js/core/events.js +11 -0
  11. package/public/js/core/plugin-loader.js +96 -11
  12. package/public/js/core/store.js +11 -0
  13. package/public/js/main.js +1 -0
  14. package/public/js/panels/assistant-bot.js +16 -0
  15. package/public/js/panels/dev-docs.js +2 -2
  16. package/public/js/panels/memory.js +1 -0
  17. package/public/js/ui/context-gauge.js +10 -1
  18. package/public/js/ui/header-dropdowns.js +30 -0
  19. package/public/js/ui/input-meta.js +13 -6
  20. package/public/js/ui/max-turns.js +6 -3
  21. package/public/js/ui/model-selector.js +1 -0
  22. package/public/js/ui/permissions.js +1 -0
  23. package/public/js/ui/tab-sdk.js +395 -176
  24. package/public/style.css +1 -0
  25. package/server/memory-optimizer.js +17 -13
  26. package/server/routes/marketplace.js +316 -0
  27. package/server/ws-handler.js +22 -15
  28. package/server.js +18 -0
  29. package/plugins/event-stream/client.css +0 -207
  30. package/plugins/event-stream/client.js +0 -271
  31. package/plugins/sudoku/client.css +0 -196
  32. package/plugins/sudoku/client.js +0 -329
  33. package/plugins/tasks/client.css +0 -414
  34. package/plugins/tasks/client.js +0 -394
  35. package/plugins/tasks/server.js +0 -116
  36. package/plugins/tic-tac-toe/client.css +0 -167
  37. package/plugins/tic-tac-toe/client.js +0 -241
package/README.md CHANGED
@@ -252,16 +252,15 @@ See [CONFIGURATION.md](docs/CONFIGURATION.md) for the full guide.
252
252
 
253
253
  ## Plugins
254
254
 
255
- Claudeck includes 7 built-in plugins and supports user plugins via `~/.claudeck/plugins/`:
255
+ Claudeck includes 3 built-in plugins, a community marketplace, and supports user plugins via `~/.claudeck/plugins/`:
256
256
 
257
257
  | Plugin | Description |
258
258
  |--------|-------------|
259
- | **Tasks** | Todo list with priority, archive, and brag tracking |
259
+ | **Claude Editor** | Edit CLAUDE.md project instructions in-app |
260
260
  | **Linear** | Linear issue tracking with team management |
261
261
  | **Repos** | Repository management with tree view |
262
- | **Claude Editor** | Edit CLAUDE.md project instructions in-app |
263
- | **Event Stream** | Real-time WebSocket event viewer |
264
- | **Games** | Tic-tac-toe and Sudoku |
262
+
263
+ **Community Marketplace** — browse, install, and update community plugins directly from the Plugin Marketplace tab. Plugins like Tasks, Event Stream, Sudoku, and Tic-Tac-Toe are available from the [marketplace](https://github.com/hamedafarag/claudeck-marketplace).
265
264
 
266
265
  **Create your own** — drop files in `~/.claudeck/plugins/<name>/` (persists across upgrades) with `client.js` and optionally `server.js`, `client.css`, `config.json`. No fork needed. See [CONFIGURATION.md](docs/CONFIGURATION.md#user-plugins) for details.
267
266
 
@@ -280,6 +279,7 @@ npx skills add https://github.com/hamedafarag/claudeck-skills
280
279
  | Document | Description |
281
280
  |----------|-------------|
282
281
  | [DOCUMENTATION.md](docs/DOCUMENTATION.md) | Full feature docs, API reference, database schema |
282
+ | [TAB-SDK.md](docs/TAB-SDK.md) | Plugin SDK reference — ctx API, events, state, CSS tokens, examples |
283
283
  | [CONFIGURATION.md](docs/CONFIGURATION.md) | User data directory, config files, plugin system |
284
284
  | [AGENT-ARCHITECTURE.md](docs/AGENT-ARCHITECTURE.md) | How agents, chains, DAGs, and orchestrator work |
285
285
  | [PLAN-sqlite-adapter.md](docs/PLAN-sqlite-adapter.md) | Database adapter pattern, async interface, multi-DB roadmap |
@@ -305,7 +305,7 @@ npm run test:perf # WebSocket performance benchmarks
305
305
  | **panels/** | 150+ | 35% |
306
306
  | **server/** | 1,350+ | 95% |
307
307
 
308
- 19 Web Components in `public/js/components/` — each is a self-contained Custom Element (Light DOM) that owns its HTML, testable with zero mocks.
308
+ 20 Web Components in `public/js/components/` — each is a self-contained Custom Element (Light DOM) that owns its HTML, testable with zero mocks.
309
309
 
310
310
  ### Performance Benchmarks
311
311
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeck",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "type": "module",
5
5
  "description": "A browser-based UI for Claude Code — chat, run workflows, manage MCP servers, track costs, and orchestrate autonomous agents from a local web interface. Installable as a PWA.",
6
6
  "main": "server.js",
@@ -0,0 +1,10 @@
1
+ {
2
+ "id": "claude-editor",
3
+ "name": "CLAUDE.md Editor",
4
+ "version": "1.0.0",
5
+ "description": "Edit CLAUDE.md project instructions directly in the UI",
6
+ "author": "claudeck",
7
+ "icon": "📝",
8
+ "hasServer": false,
9
+ "minClaudeckVersion": "1.4.0"
10
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "id": "linear",
3
+ "name": "Linear",
4
+ "version": "1.0.0",
5
+ "description": "Linear issue tracking with settings and team management",
6
+ "author": "claudeck",
7
+ "icon": "📋",
8
+ "hasServer": true,
9
+ "minClaudeckVersion": "1.4.0"
10
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "id": "repos",
3
+ "name": "Repos",
4
+ "version": "1.0.0",
5
+ "description": "Git repository and group management with tree view",
6
+ "author": "claudeck",
7
+ "icon": "📁",
8
+ "hasServer": true,
9
+ "minClaudeckVersion": "1.4.0"
10
+ }
@@ -486,3 +486,210 @@
486
486
  filter: brightness(1.1);
487
487
  box-shadow: var(--glow-strong);
488
488
  }
489
+
490
+ /* ── Marketplace tabs ── */
491
+
492
+ .marketplace-tabs {
493
+ display: flex;
494
+ gap: 4px;
495
+ margin-top: 12px;
496
+ }
497
+
498
+ .marketplace-tab {
499
+ padding: 6px 16px;
500
+ border-radius: var(--radius-md);
501
+ font-size: 12px;
502
+ font-weight: 500;
503
+ font-family: var(--font-sans);
504
+ cursor: pointer;
505
+ transition: all 0.2s var(--ease-smooth);
506
+ border: 1px solid transparent;
507
+ background: none;
508
+ color: var(--text-dim);
509
+ }
510
+
511
+ .marketplace-tab:hover {
512
+ color: var(--text);
513
+ background: var(--bg-tertiary);
514
+ }
515
+
516
+ .marketplace-tab.active {
517
+ color: var(--accent);
518
+ background: var(--accent-dim);
519
+ border-color: var(--accent);
520
+ }
521
+
522
+ .marketplace-tab-content {
523
+ flex: 1;
524
+ overflow-y: auto;
525
+ display: flex;
526
+ flex-direction: column;
527
+ }
528
+
529
+ .marketplace-tab-content::-webkit-scrollbar {
530
+ width: 4px;
531
+ }
532
+ .marketplace-tab-content::-webkit-scrollbar-thumb {
533
+ background: var(--border);
534
+ border-radius: 2px;
535
+ }
536
+
537
+ .marketplace-tab-content > .marketplace-subtitle {
538
+ padding: 12px 16px 4px;
539
+ }
540
+
541
+ .marketplace-tab-content > .marketplace-list {
542
+ flex: 1;
543
+ }
544
+
545
+ .marketplace-tab-content > .marketplace-empty {
546
+ flex: 1;
547
+ display: flex;
548
+ flex-direction: column;
549
+ align-items: center;
550
+ justify-content: center;
551
+ }
552
+
553
+ .marketplace-tab-content > .marketplace-empty a {
554
+ color: var(--accent);
555
+ text-decoration: none;
556
+ margin-top: 8px;
557
+ }
558
+
559
+ .marketplace-tab-content > .marketplace-empty a:hover {
560
+ text-decoration: underline;
561
+ }
562
+
563
+ /* ── Community plugin items ── */
564
+
565
+ .marketplace-community-item {
566
+ cursor: default;
567
+ padding: 12px;
568
+ }
569
+
570
+ .marketplace-item-meta {
571
+ display: flex;
572
+ gap: 8px;
573
+ margin-top: 4px;
574
+ font-size: 10px;
575
+ color: var(--text-dim);
576
+ font-family: var(--font-mono);
577
+ }
578
+
579
+ .marketplace-author {
580
+ opacity: 0.7;
581
+ }
582
+
583
+ .marketplace-version {
584
+ opacity: 0.5;
585
+ }
586
+
587
+ .marketplace-version-old {
588
+ opacity: 0.4;
589
+ text-decoration: line-through;
590
+ }
591
+
592
+ .marketplace-source {
593
+ font-size: 9px;
594
+ font-family: var(--font-display);
595
+ padding: 1px 6px;
596
+ border-radius: 4px;
597
+ text-transform: uppercase;
598
+ letter-spacing: 0.06em;
599
+ vertical-align: middle;
600
+ }
601
+
602
+ .marketplace-source.community {
603
+ color: var(--purple);
604
+ background: color-mix(in srgb, var(--purple) 15%, transparent);
605
+ border: 1px solid color-mix(in srgb, var(--purple) 30%, transparent);
606
+ }
607
+
608
+ .marketplace-source.server {
609
+ color: var(--orange, #f0a040);
610
+ background: color-mix(in srgb, var(--orange, #f0a040) 15%, transparent);
611
+ border: 1px solid color-mix(in srgb, var(--orange, #f0a040) 30%, transparent);
612
+ }
613
+
614
+ /* ── Action buttons (install / uninstall / update) ── */
615
+
616
+ .marketplace-item-actions {
617
+ flex-shrink: 0;
618
+ }
619
+
620
+ .marketplace-action-btn {
621
+ padding: 5px 14px;
622
+ border-radius: var(--radius-md);
623
+ font-size: 11px;
624
+ font-weight: 500;
625
+ font-family: var(--font-sans);
626
+ cursor: pointer;
627
+ transition: all 0.2s var(--ease-smooth);
628
+ border: 1px solid var(--border);
629
+ white-space: nowrap;
630
+ }
631
+
632
+ .marketplace-action-btn:disabled {
633
+ opacity: 0.5;
634
+ cursor: not-allowed;
635
+ }
636
+
637
+ .marketplace-install-btn {
638
+ background: var(--accent);
639
+ color: #fff;
640
+ border-color: var(--accent);
641
+ }
642
+
643
+ .marketplace-install-btn:hover:not(:disabled) {
644
+ filter: brightness(1.1);
645
+ box-shadow: var(--glow);
646
+ }
647
+
648
+ .marketplace-uninstall-btn {
649
+ background: none;
650
+ color: var(--text-dim);
651
+ }
652
+
653
+ .marketplace-uninstall-btn:hover:not(:disabled) {
654
+ color: var(--red, #e54);
655
+ border-color: var(--red, #e54);
656
+ background: color-mix(in srgb, var(--red, #e54) 10%, transparent);
657
+ }
658
+
659
+ .marketplace-update-btn {
660
+ background: var(--purple);
661
+ color: #fff;
662
+ border-color: var(--purple);
663
+ }
664
+
665
+ .marketplace-update-btn:hover:not(:disabled) {
666
+ filter: brightness(1.1);
667
+ box-shadow: 0 0 8px color-mix(in srgb, var(--purple) 40%, transparent);
668
+ }
669
+
670
+ /* ── Loading state ── */
671
+
672
+ .marketplace-loading {
673
+ display: flex;
674
+ flex-direction: column;
675
+ align-items: center;
676
+ justify-content: center;
677
+ gap: 12px;
678
+ padding: 48px 16px;
679
+ color: var(--text-dim);
680
+ font-size: 12px;
681
+ font-family: var(--font-sans);
682
+ }
683
+
684
+ .marketplace-spinner {
685
+ width: 24px;
686
+ height: 24px;
687
+ border: 2px solid var(--border);
688
+ border-top-color: var(--accent);
689
+ border-radius: 50%;
690
+ animation: spin 0.8s linear infinite;
691
+ }
692
+
693
+ @keyframes spin {
694
+ to { transform: rotate(360deg); }
695
+ }
@@ -0,0 +1,75 @@
1
+ .settings-modal {
2
+ width: 380px;
3
+ max-width: 90vw;
4
+ }
5
+
6
+ .settings-list {
7
+ padding: 16px;
8
+ }
9
+
10
+ .settings-row {
11
+ display: flex;
12
+ align-items: center;
13
+ justify-content: space-between;
14
+ gap: 12px;
15
+ padding: 10px 0;
16
+ cursor: pointer;
17
+ }
18
+
19
+ .settings-row + .settings-row {
20
+ border-top: 1px solid var(--border);
21
+ }
22
+
23
+ .settings-label {
24
+ display: flex;
25
+ flex-direction: column;
26
+ gap: 2px;
27
+ }
28
+
29
+ .settings-label strong {
30
+ font-size: 13px;
31
+ color: var(--text-primary);
32
+ }
33
+
34
+ .settings-label small {
35
+ font-size: 11px;
36
+ color: var(--text-secondary);
37
+ }
38
+
39
+ .settings-toggle {
40
+ appearance: none;
41
+ width: 36px;
42
+ height: 20px;
43
+ background: var(--bg-tertiary);
44
+ border-radius: 10px;
45
+ position: relative;
46
+ cursor: pointer;
47
+ flex-shrink: 0;
48
+ transition: background 0.2s;
49
+ }
50
+
51
+ .settings-toggle:focus-visible {
52
+ outline: 2px solid var(--accent);
53
+ outline-offset: 2px;
54
+ }
55
+
56
+ .settings-toggle::after {
57
+ content: '';
58
+ position: absolute;
59
+ top: 2px;
60
+ left: 2px;
61
+ width: 16px;
62
+ height: 16px;
63
+ border-radius: 50%;
64
+ background: var(--text-secondary);
65
+ transition: transform 0.2s, background 0.2s;
66
+ }
67
+
68
+ .settings-toggle:checked {
69
+ background: var(--accent);
70
+ }
71
+
72
+ .settings-toggle:checked::after {
73
+ transform: translateX(16px);
74
+ background: #fff;
75
+ }
package/public/index.html CHANGED
@@ -166,6 +166,12 @@
166
166
  <span id="telegram-label">Telegram</span>
167
167
  </button>
168
168
  <div style="height:1px;background:var(--border);margin:4px 8px;"></div>
169
+ <button class="header-dropdown-item" id="settings-btn" title="Settings">
170
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
171
+ <line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/>
172
+ </svg>
173
+ <span>Settings</span>
174
+ </button>
169
175
  <button class="header-dropdown-item" id="dev-docs-btn" title="Developer Documentation">
170
176
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
171
177
  <path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>
@@ -519,6 +525,7 @@
519
525
  <claudeck-linear-create></claudeck-linear-create>
520
526
  <claudeck-telegram-modal></claudeck-telegram-modal>
521
527
  <claudeck-mcp-modal></claudeck-mcp-modal>
528
+ <claudeck-settings-modal></claudeck-settings-modal>
522
529
  <claudeck-add-project></claudeck-add-project>
523
530
 
524
531
  <!-- Status Bar (Web Component) -->
@@ -0,0 +1,65 @@
1
+ // Web Component: Settings Modal
2
+ const SETTINGS_KEY = 'claudeck-settings';
3
+
4
+ function loadSettings() {
5
+ try { return JSON.parse(localStorage.getItem(SETTINGS_KEY) || '{}'); } catch { return {}; }
6
+ }
7
+
8
+ function saveSettings(s) {
9
+ localStorage.setItem(SETTINGS_KEY, JSON.stringify(s));
10
+ }
11
+
12
+ export function getSetting(key, fallback = true) {
13
+ const s = loadSettings();
14
+ return s[key] !== undefined ? s[key] : fallback;
15
+ }
16
+
17
+ class ClaudeckSettingsModal extends HTMLElement {
18
+ connectedCallback() {
19
+ this.innerHTML = `
20
+ <div id="settings-modal" class="modal-overlay hidden">
21
+ <div class="modal settings-modal">
22
+ <div class="modal-header">
23
+ <h3>Settings</h3>
24
+ <button id="settings-modal-close" class="modal-close">&times;</button>
25
+ </div>
26
+ <div class="settings-list">
27
+ <label class="settings-row">
28
+ <span class="settings-label">
29
+ <strong>Assistant Bot</strong>
30
+ <small>Show the floating assistant bot bubble</small>
31
+ </span>
32
+ <input type="checkbox" id="setting-assistant-bot" class="settings-toggle">
33
+ </label>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ `;
38
+
39
+ const overlay = this.querySelector('#settings-modal');
40
+ const closeBtn = this.querySelector('#settings-modal-close');
41
+ const botToggle = this.querySelector('#setting-assistant-bot');
42
+
43
+ // Init toggle state
44
+ botToggle.checked = getSetting('assistantBot', true);
45
+
46
+ closeBtn.addEventListener('click', () => overlay.classList.add('hidden'));
47
+ overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.classList.add('hidden'); });
48
+
49
+ botToggle.addEventListener('change', () => {
50
+ const s = loadSettings();
51
+ s.assistantBot = botToggle.checked;
52
+ saveSettings(s);
53
+ // Dispatch event so the bot module can react
54
+ window.dispatchEvent(new CustomEvent('setting:assistantBot', { detail: botToggle.checked }));
55
+ });
56
+
57
+ // Open from settings button
58
+ document.getElementById('settings-btn')?.addEventListener('click', () => {
59
+ botToggle.checked = getSetting('assistantBot', true);
60
+ overlay.classList.remove('hidden');
61
+ });
62
+ }
63
+ }
64
+
65
+ customElements.define('claudeck-settings-modal', ClaudeckSettingsModal);
@@ -5,6 +5,17 @@ export function emit(event, data) {
5
5
  (bus[event] || []).forEach((fn) => fn(data));
6
6
  }
7
7
 
8
+ /** Subscribe to an event. Returns an unsubscribe function. */
8
9
  export function on(event, fn) {
9
10
  (bus[event] ||= []).push(fn);
11
+ return () => {
12
+ const arr = bus[event];
13
+ if (arr) bus[event] = arr.filter(f => f !== fn);
14
+ };
15
+ }
16
+
17
+ /** Remove a specific listener for an event. */
18
+ export function off(event, fn) {
19
+ const arr = bus[event];
20
+ if (arr) bus[event] = arr.filter(f => f !== fn);
10
21
  }
@@ -12,21 +12,13 @@
12
12
  const STORAGE_KEY = 'claudeck-enabled-plugins';
13
13
  const ORDER_KEY = 'claudeck-plugin-order';
14
14
  let availablePlugins = [];
15
+ let marketplaceRegistry = null;
15
16
  const loadedPlugins = new Set();
16
17
 
17
18
  /** Maps plugin file name → tab ID registered by that plugin */
18
19
  const pluginTabIds = new Map();
19
20
 
20
- /** Plugin descriptions for the marketplace. order: lower = higher in the list */
21
- const pluginMeta = {
22
- 'claude-editor': { description: 'Edit CLAUDE.md project instructions directly in the UI', icon: '📝', order: 5 },
23
- 'event-stream': { description: 'Real-time WebSocket event viewer with filtering and search', icon: '⚡', order: 10 },
24
- 'repos': { description: 'Git repository and group management with tree view', icon: '📁', order: 20 },
25
- 'linear': { description: 'Linear issue tracking with settings and team management', icon: '📋', order: 25 },
26
- 'tasks': { description: 'Todo list with priority levels and brag tracking', icon: '✅', order: 30 },
27
- 'tic-tac-toe': { description: 'Classic tic-tac-toe game', icon: '🎮', order: 90 },
28
- 'sudoku': { description: 'Sudoku puzzle game', icon: '🧩', order: 91 },
29
- };
21
+ /** Fallback meta for plugins without manifest.json */
30
22
  const defaultMeta = { description: 'A tab-sdk plugin', icon: '🧩', order: 100 };
31
23
 
32
24
  export function getAvailablePlugins() {
@@ -45,7 +37,15 @@ export function setEnabledPluginNames(names) {
45
37
  }
46
38
 
47
39
  export function getPluginMeta(name) {
48
- return pluginMeta[name] || defaultMeta;
40
+ const plugin = availablePlugins.find(p => p.name === name);
41
+ if (plugin?.manifest) {
42
+ return {
43
+ description: plugin.manifest.description || defaultMeta.description,
44
+ icon: plugin.manifest.icon || defaultMeta.icon,
45
+ order: defaultMeta.order,
46
+ };
47
+ }
48
+ return defaultMeta;
49
49
  }
50
50
 
51
51
  export function getPluginOrder() {
@@ -151,3 +151,88 @@ export async function loadPlugins() {
151
151
  console.error('Plugin loader error:', err);
152
152
  }
153
153
  }
154
+
155
+ // ── Marketplace ─────────────────────────────────────────
156
+
157
+ export function getMarketplaceRegistry() {
158
+ return marketplaceRegistry;
159
+ }
160
+
161
+ /** Fetch the community plugin registry from the server (which proxies GitHub) */
162
+ export async function fetchMarketplace(refresh = false) {
163
+ try {
164
+ const url = refresh ? '/api/marketplace?refresh=true' : '/api/marketplace';
165
+ const res = await fetch(url);
166
+ if (!res.ok) { console.warn('Marketplace fetch failed:', res.status); return null; }
167
+ marketplaceRegistry = await res.json();
168
+ return marketplaceRegistry;
169
+ } catch (err) {
170
+ console.error('Marketplace error:', err);
171
+ return null;
172
+ }
173
+ }
174
+
175
+ /** Install a community plugin and auto-enable it */
176
+ export async function installMarketplacePlugin(plugin) {
177
+ const res = await fetch('/api/marketplace/install', {
178
+ method: 'POST',
179
+ headers: { 'Content-Type': 'application/json' },
180
+ body: JSON.stringify({ id: plugin.id, repo: plugin.repo, source: plugin.source }),
181
+ });
182
+ if (!res.ok) {
183
+ const err = await res.json().catch(() => ({ error: 'Install failed' }));
184
+ throw new Error(err.error);
185
+ }
186
+ const result = await res.json();
187
+
188
+ // Refresh local plugins list to include the newly installed plugin
189
+ const pluginsRes = await fetch('/api/plugins');
190
+ if (pluginsRes.ok) {
191
+ availablePlugins = await pluginsRes.json();
192
+ }
193
+
194
+ // Auto-enable the newly installed plugin
195
+ const enabled = getEnabledPluginNames();
196
+ if (!enabled.includes(plugin.id)) {
197
+ enabled.push(plugin.id);
198
+ setEnabledPluginNames(enabled);
199
+ }
200
+
201
+ // Load the plugin immediately
202
+ await loadPluginByName(plugin.id);
203
+
204
+ return result;
205
+ }
206
+
207
+ /** Uninstall a community plugin */
208
+ export async function uninstallMarketplacePlugin(id) {
209
+ const res = await fetch('/api/marketplace/uninstall', {
210
+ method: 'POST',
211
+ headers: { 'Content-Type': 'application/json' },
212
+ body: JSON.stringify({ id }),
213
+ });
214
+ if (!res.ok) {
215
+ const err = await res.json().catch(() => ({ error: 'Uninstall failed' }));
216
+ throw new Error(err.error);
217
+ }
218
+
219
+ // Remove from enabled list
220
+ const enabled = getEnabledPluginNames().filter(n => n !== id);
221
+ setEnabledPluginNames(enabled);
222
+
223
+ // Clean up CSS link from DOM
224
+ const cssLink = document.head.querySelector(`link[data-plugin="${id}"]`);
225
+ if (cssLink) cssLink.remove();
226
+
227
+ // Clear loaded/tab tracking state
228
+ loadedPlugins.delete(id);
229
+ pluginTabIds.delete(id);
230
+
231
+ // Refresh local plugins list
232
+ const pluginsRes = await fetch('/api/plugins');
233
+ if (pluginsRes.ok) {
234
+ availablePlugins = await pluginsRes.json();
235
+ }
236
+
237
+ return await res.json();
238
+ }
@@ -30,8 +30,19 @@ export function setState(key, val) {
30
30
  emit(key, val);
31
31
  }
32
32
 
33
+ /** Subscribe to state changes for a key. Returns an unsubscribe function. */
33
34
  export function on(key, fn) {
34
35
  (listeners[key] ||= []).push(fn);
36
+ return () => {
37
+ const arr = listeners[key];
38
+ if (arr) listeners[key] = arr.filter(f => f !== fn);
39
+ };
40
+ }
41
+
42
+ /** Remove a specific listener for a key. */
43
+ export function off(key, fn) {
44
+ const arr = listeners[key];
45
+ if (arr) listeners[key] = arr.filter(f => f !== fn);
35
46
  }
36
47
 
37
48
  function emit(key, val) {
package/public/js/main.js CHANGED
@@ -18,6 +18,7 @@ import './components/permission-modal.js';
18
18
  import './components/linear-create-modal.js';
19
19
  import './components/telegram-modal.js';
20
20
  import './components/mcp-modal.js';
21
+ import './components/settings-modal.js';
21
22
  import './components/add-project-modal.js';
22
23
  import './components/status-bar.js';
23
24
 
@@ -6,6 +6,7 @@ import { renderMarkdown, highlightCodeBlocks, addCopyButtons } from '../ui/forma
6
6
  import * as api from '../core/api.js';
7
7
  import { getSelectedModel } from '../ui/model-selector.js';
8
8
  import { $ } from '../core/dom.js';
9
+ import { getSetting } from '../components/settings-modal.js';
9
10
 
10
11
  const SESSIONS_KEY = 'claudeck-bot-sessions';
11
12
  let panel, messagesDiv, inputEl, sendBtn, stopBtn, settingsOverlay, promptTextarea;
@@ -433,12 +434,27 @@ function newBotSession() {
433
434
  showBotWhaly();
434
435
  }
435
436
 
437
+ // ── Visibility ──────────────────────────────────────────
438
+
439
+ let bubble; // reference set in createBotDOM
440
+
441
+ function setBotVisible(visible) {
442
+ if (!bubble) return;
443
+ bubble.style.display = visible ? '' : 'none';
444
+ if (!visible && panel) closePanel();
445
+ }
446
+
436
447
  // ── Init ────────────────────────────────────────────────
437
448
 
438
449
  function init() {
439
450
  createBotDOM();
451
+ bubble = document.querySelector('.bot-bubble');
440
452
  on('ws:message', handleBotWsMessage);
441
453
  fetchSystemPrompt();
454
+
455
+ // Respect enable/disable setting
456
+ setBotVisible(getSetting('assistantBot', true));
457
+ window.addEventListener('setting:assistantBot', (e) => setBotVisible(e.detail));
442
458
  }
443
459
 
444
460
  // Run on import
@@ -147,7 +147,7 @@ registerTab({
147
147
  <li>Existing shortcuts (e.g. <code>openRightPanel('my-tab')</code>) work automatically</li>
148
148
  </ul>
149
149
 
150
- <div class="callout">See <code>plugins/event-stream/client.js</code> for a complete working example of a plugin tab.</div>
150
+ <div class="callout">See <code>plugins/claude-editor/client.js</code> for a complete working example of a plugin tab.</div>
151
151
  `,
152
152
  });
153
153
 
@@ -301,7 +301,7 @@ registerDocSection({
301
301
  <li>Import paths: use absolute paths (e.g. <code>/js/ui/tab-sdk.js</code>)</li>
302
302
  </ul>
303
303
 
304
- <div class="callout">When in doubt, look at <code>plugins/event-stream/</code>, <code>plugins/repos/</code>, or <code>plugins/tasks/</code> as reference implementations. For full-stack with server routes, see <code>plugins/linear/</code>.</div>
304
+ <div class="callout">When in doubt, look at <code>plugins/claude-editor/</code> or <code>plugins/repos/</code> as reference implementations. For full-stack with server routes, see <code>plugins/linear/</code>. More examples are available in the <a href="https://github.com/hamedafarag/claudeck-marketplace" target="_blank">marketplace repo</a>.</div>
305
305
  `,
306
306
  });
307
307