claudeck 1.0.0

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 (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +233 -0
  3. package/cli.js +2 -0
  4. package/config/agent-chains.json +16 -0
  5. package/config/agent-dags.json +16 -0
  6. package/config/agents.json +46 -0
  7. package/config/bot-prompt.json +3 -0
  8. package/config/folders.json +66 -0
  9. package/config/prompts.json +92 -0
  10. package/config/repos.json +86 -0
  11. package/config/telegram-config.json +17 -0
  12. package/config/workflows.json +90 -0
  13. package/db.js +1198 -0
  14. package/package.json +55 -0
  15. package/plugins/claude-editor/client.css +171 -0
  16. package/plugins/claude-editor/client.js +183 -0
  17. package/plugins/event-stream/client.css +207 -0
  18. package/plugins/event-stream/client.js +271 -0
  19. package/plugins/linear/client.css +345 -0
  20. package/plugins/linear/client.js +380 -0
  21. package/plugins/linear/config.json +5 -0
  22. package/plugins/linear/server.js +312 -0
  23. package/plugins/repos/client.css +549 -0
  24. package/plugins/repos/client.js +663 -0
  25. package/plugins/repos/server.js +232 -0
  26. package/plugins/sudoku/client.css +196 -0
  27. package/plugins/sudoku/client.js +329 -0
  28. package/plugins/tasks/client.css +414 -0
  29. package/plugins/tasks/client.js +394 -0
  30. package/plugins/tasks/server.js +116 -0
  31. package/plugins/tic-tac-toe/client.css +167 -0
  32. package/plugins/tic-tac-toe/client.js +241 -0
  33. package/public/css/core/components.css +232 -0
  34. package/public/css/core/layout.css +330 -0
  35. package/public/css/core/print.css +18 -0
  36. package/public/css/core/reset.css +36 -0
  37. package/public/css/core/responsive.css +378 -0
  38. package/public/css/core/theme.css +116 -0
  39. package/public/css/core/variables.css +93 -0
  40. package/public/css/features/agent-monitor.css +297 -0
  41. package/public/css/features/agent-sidebar.css +525 -0
  42. package/public/css/features/agents.css +996 -0
  43. package/public/css/features/analytics.css +181 -0
  44. package/public/css/features/background-sessions.css +321 -0
  45. package/public/css/features/cost-dashboard.css +168 -0
  46. package/public/css/features/home.css +313 -0
  47. package/public/css/features/retro-terminal.css +88 -0
  48. package/public/css/features/telegram.css +127 -0
  49. package/public/css/features/tour.css +148 -0
  50. package/public/css/features/voice-input.css +60 -0
  51. package/public/css/features/welcome.css +241 -0
  52. package/public/css/panels/assistant-bot.css +442 -0
  53. package/public/css/panels/dev-docs.css +292 -0
  54. package/public/css/panels/file-explorer.css +322 -0
  55. package/public/css/panels/git-panel.css +221 -0
  56. package/public/css/panels/mcp-manager.css +199 -0
  57. package/public/css/panels/tips-feed.css +353 -0
  58. package/public/css/ui/commands.css +273 -0
  59. package/public/css/ui/context-gauge.css +76 -0
  60. package/public/css/ui/file-picker.css +69 -0
  61. package/public/css/ui/image-attachments.css +106 -0
  62. package/public/css/ui/messages.css +884 -0
  63. package/public/css/ui/modals.css +122 -0
  64. package/public/css/ui/parallel.css +217 -0
  65. package/public/css/ui/permissions.css +110 -0
  66. package/public/css/ui/right-panel.css +481 -0
  67. package/public/css/ui/sessions.css +689 -0
  68. package/public/css/ui/status-bar.css +425 -0
  69. package/public/css/ui/toolbox.css +206 -0
  70. package/public/data/tips.json +218 -0
  71. package/public/icons/favicon.png +0 -0
  72. package/public/icons/icon-192.png +0 -0
  73. package/public/icons/icon-512.png +0 -0
  74. package/public/icons/whaly.png +0 -0
  75. package/public/index.html +1140 -0
  76. package/public/js/core/api.js +591 -0
  77. package/public/js/core/constants.js +3 -0
  78. package/public/js/core/dom.js +270 -0
  79. package/public/js/core/events.js +10 -0
  80. package/public/js/core/plugin-loader.js +153 -0
  81. package/public/js/core/store.js +39 -0
  82. package/public/js/core/utils.js +25 -0
  83. package/public/js/core/ws.js +64 -0
  84. package/public/js/features/agent-monitor.js +222 -0
  85. package/public/js/features/agents.js +1209 -0
  86. package/public/js/features/analytics.js +397 -0
  87. package/public/js/features/attachments.js +251 -0
  88. package/public/js/features/background-sessions.js +475 -0
  89. package/public/js/features/chat.js +589 -0
  90. package/public/js/features/cost-dashboard.js +152 -0
  91. package/public/js/features/dag-editor.js +399 -0
  92. package/public/js/features/easter-egg.js +46 -0
  93. package/public/js/features/home.js +270 -0
  94. package/public/js/features/projects.js +372 -0
  95. package/public/js/features/prompts.js +228 -0
  96. package/public/js/features/sessions.js +332 -0
  97. package/public/js/features/telegram.js +131 -0
  98. package/public/js/features/tour.js +210 -0
  99. package/public/js/features/voice-input.js +185 -0
  100. package/public/js/features/welcome.js +43 -0
  101. package/public/js/features/workflows.js +277 -0
  102. package/public/js/main.js +51 -0
  103. package/public/js/panels/assistant-bot.js +445 -0
  104. package/public/js/panels/dev-docs.js +380 -0
  105. package/public/js/panels/file-explorer.js +486 -0
  106. package/public/js/panels/git-panel.js +285 -0
  107. package/public/js/panels/mcp-manager.js +311 -0
  108. package/public/js/panels/tips-feed.js +303 -0
  109. package/public/js/ui/commands.js +114 -0
  110. package/public/js/ui/context-gauge.js +100 -0
  111. package/public/js/ui/diff.js +124 -0
  112. package/public/js/ui/disabled-tools.js +36 -0
  113. package/public/js/ui/export.js +74 -0
  114. package/public/js/ui/formatting.js +206 -0
  115. package/public/js/ui/header-dropdowns.js +72 -0
  116. package/public/js/ui/input-meta.js +71 -0
  117. package/public/js/ui/max-turns.js +21 -0
  118. package/public/js/ui/messages.js +387 -0
  119. package/public/js/ui/model-selector.js +20 -0
  120. package/public/js/ui/notifications.js +232 -0
  121. package/public/js/ui/parallel.js +176 -0
  122. package/public/js/ui/permissions.js +168 -0
  123. package/public/js/ui/right-panel.js +173 -0
  124. package/public/js/ui/shortcuts.js +143 -0
  125. package/public/js/ui/sidebar-toggle.js +29 -0
  126. package/public/js/ui/status-bar.js +172 -0
  127. package/public/js/ui/tab-sdk.js +623 -0
  128. package/public/js/ui/theme.js +38 -0
  129. package/public/manifest.json +13 -0
  130. package/public/offline.html +190 -0
  131. package/public/style.css +42 -0
  132. package/public/sw.js +91 -0
  133. package/server/agent-loop.js +385 -0
  134. package/server/dag-executor.js +265 -0
  135. package/server/orchestrator.js +514 -0
  136. package/server/paths.js +61 -0
  137. package/server/plugin-mount.js +56 -0
  138. package/server/push-sender.js +31 -0
  139. package/server/routes/agents.js +294 -0
  140. package/server/routes/bot.js +45 -0
  141. package/server/routes/exec.js +35 -0
  142. package/server/routes/files.js +218 -0
  143. package/server/routes/mcp.js +82 -0
  144. package/server/routes/messages.js +36 -0
  145. package/server/routes/notifications.js +37 -0
  146. package/server/routes/projects.js +207 -0
  147. package/server/routes/prompts.js +53 -0
  148. package/server/routes/sessions.js +103 -0
  149. package/server/routes/stats.js +143 -0
  150. package/server/routes/telegram.js +71 -0
  151. package/server/routes/tips.js +135 -0
  152. package/server/routes/workflows.js +81 -0
  153. package/server/summarizer.js +55 -0
  154. package/server/telegram-poller.js +205 -0
  155. package/server/telegram-sender.js +304 -0
  156. package/server/ws-handler.js +926 -0
  157. package/server.js +179 -0
@@ -0,0 +1,623 @@
1
+ // ╔══════════════════════════════════════════════════════════════╗
2
+ // ║ Tab SDK — API Guide ║
3
+ // ╚══════════════════════════════════════════════════════════════╝
4
+ //
5
+ // Register custom tabs in the right panel with a single function call.
6
+ // No HTML or dom.js changes needed — the SDK handles DOM creation,
7
+ // lifecycle hooks, badges, and state management automatically.
8
+ //
9
+ // ── Quick Start ─────────────────────────────────────────────────
10
+ //
11
+ // // plugins/my-tab/client.js
12
+ // import { registerTab } from '/js/ui/tab-sdk.js';
13
+ //
14
+ // registerTab({
15
+ // id: 'my-tab',
16
+ // title: 'My Tab',
17
+ // icon: '<svg>...</svg>', // optional, 12×12 recommended
18
+ // lazy: true, // defer init until first open
19
+ //
20
+ // init(ctx) {
21
+ // const root = document.createElement('div');
22
+ // root.textContent = 'Hello from my tab!';
23
+ //
24
+ // // Listen for live WebSocket messages
25
+ // ctx.on('ws:message', (msg) => { /* ... */ });
26
+ //
27
+ // // React to state changes (e.g. session switch)
28
+ // ctx.onState('sessionId', (id) => { /* reload data */ });
29
+ //
30
+ // // Show a badge count on the tab button
31
+ // ctx.showBadge(5);
32
+ //
33
+ // return root; // must return an HTMLElement
34
+ // },
35
+ //
36
+ // onActivate() { /* tab became visible */ },
37
+ // onDeactivate() { /* tab was hidden */ },
38
+ // onDestroy() { /* tab unregistered — cleanup */ },
39
+ // });
40
+ //
41
+ // // Auto-discovered — no main.js changes needed!
42
+ //
43
+ // ── registerTab(config) ─────────────────────────────────────────
44
+ //
45
+ // config.id {string} Required. Unique identifier (data-tab value)
46
+ // config.title {string} Required. Button label
47
+ // config.icon {string} Optional. SVG/HTML for tab icon
48
+ // config.position {number} Optional. 0-based insert index. Omit = append
49
+ // config.shortcut {string} Optional. Informational shortcut label
50
+ // config.lazy {boolean} Optional. Default false. Defer init() to first open
51
+ // config.init(ctx) {function} Required. Returns HTMLElement for tab content
52
+ // config.onActivate {function} Optional. Called each time tab is shown
53
+ // config.onDeactivate {function} Optional. Called each time tab is hidden
54
+ // config.onDestroy {function} Optional. Called when tab is unregistered
55
+ //
56
+ // ── Context object (ctx) — passed to init() ─────────────────────
57
+ //
58
+ // ctx.on(event, fn) Subscribe to the app event bus
59
+ // ctx.emit(event, data) Publish to the app event bus
60
+ // ctx.getState(key) Read from the reactive store
61
+ // ctx.onState(key, fn) Subscribe to store changes
62
+ // ctx.api The full API module (fetch helpers)
63
+ // ctx.getProjectPath() Current project path
64
+ // ctx.getSessionId() Current session ID
65
+ // ctx.showBadge(count) Show a number badge on the tab button
66
+ // ctx.clearBadge() Hide the badge
67
+ // ctx.setTitle(text) Update the tab button label at runtime
68
+ //
69
+ // ── Other exports ───────────────────────────────────────────────
70
+ //
71
+ // unregisterTab(id) Remove a tab and call onDestroy
72
+ // getRegisteredTabs() Returns array of registered tab IDs
73
+ // initTabSDK() Called by right-panel.js — do not call manually
74
+ //
75
+ // ── Tips ────────────────────────────────────────────────────────
76
+ //
77
+ // • Use lazy:true for heavy tabs — init runs only on first open
78
+ // • Build all DOM in init(); no index.html edits needed
79
+ // • Use ctx.on('ws:message', fn) for real-time streaming events
80
+ // • Use ctx.onState('sessionId', fn) to reload on session switch
81
+ // • Existing shortcuts (e.g. openRightPanel('my-tab')) work automatically
82
+ // • See plugins/event-stream/client.js for a full working example
83
+ //
84
+ // ════════════════════════════════════════════════════════════════
85
+
86
+ import { $ } from '../core/dom.js';
87
+ import { emit, on } from '../core/events.js';
88
+ import { getState, on as onState } from '../core/store.js';
89
+ import * as api from '../core/api.js';
90
+ import {
91
+ getAvailablePlugins, getEnabledPluginNames, setEnabledPluginNames,
92
+ getPluginMeta, loadPluginByName, isPluginLoaded,
93
+ trackPluginTab, getPluginTabId, getPluginTabMap,
94
+ setTabIdResolver, getSortedPlugins, setPluginOrder,
95
+ } from '../core/plugin-loader.js';
96
+
97
+ const registeredTabs = new Map();
98
+ const unregisteredConfigs = new Map(); // stores configs for re-registration
99
+
100
+ // Wire up the tab ID resolver so plugin-loader can auto-detect tab IDs
101
+ setTabIdResolver(() => [...registeredTabs.keys()]);
102
+ let tabBarEl = null;
103
+ let contentEl = null;
104
+ let closeBtn = null;
105
+ let initialized = false;
106
+ const pendingTabs = [];
107
+
108
+ // ── Public API ──────────────────────────────────────────
109
+
110
+ /**
111
+ * Register a new tab in the right panel.
112
+ *
113
+ * @param {object} config
114
+ * @param {string} config.id - Unique tab identifier (used as data-tab)
115
+ * @param {string} config.title - Display title on the tab button
116
+ * @param {string} [config.icon] - SVG/HTML icon (shown before title on narrow screens)
117
+ * @param {number} [config.position] - Insert position (0-based). Omit to append at end
118
+ * @param {string} [config.shortcut] - Keyboard shortcut description (informational)
119
+ * @param {boolean} [config.lazy=false] - If true, init() is deferred until first tab open
120
+ * @param {function} config.init - Called with ctx, must return a DOM element (the tab content)
121
+ * @param {function} [config.onActivate] - Called when tab becomes visible
122
+ * @param {function} [config.onDeactivate] - Called when tab is hidden
123
+ * @param {function} [config.onDestroy] - Called on cleanup
124
+ */
125
+ export function registerTab(config) {
126
+ if (!config.id || !config.init) {
127
+ throw new Error('registerTab requires id and init');
128
+ }
129
+ if (registeredTabs.has(config.id)) {
130
+ console.warn(`Tab "${config.id}" already registered`);
131
+ return;
132
+ }
133
+
134
+ // Store original config for potential re-registration
135
+ unregisteredConfigs.set(config.id, config);
136
+
137
+ const tab = {
138
+ ...config,
139
+ lazy: config.lazy ?? false,
140
+ _initialized: false,
141
+ _paneEl: null,
142
+ _btnEl: null,
143
+ _badgeEl: null,
144
+ };
145
+
146
+ registeredTabs.set(config.id, tab);
147
+
148
+ if (initialized) {
149
+ mountTab(tab);
150
+ } else {
151
+ pendingTabs.push(tab);
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Re-register a previously unregistered tab from its stored config.
157
+ */
158
+ export function reRegisterTab(tabId) {
159
+ if (registeredTabs.has(tabId)) return true;
160
+ const config = unregisteredConfigs.get(tabId);
161
+ if (!config) return false;
162
+ registerTab(config);
163
+ return true;
164
+ }
165
+
166
+ /**
167
+ * Unregister and remove a tab.
168
+ */
169
+ export function unregisterTab(id) {
170
+ const tab = registeredTabs.get(id);
171
+ if (!tab) return;
172
+ if (tab.onDestroy) tab.onDestroy();
173
+ if (tab._btnEl) tab._btnEl.remove();
174
+ if (tab._paneEl) tab._paneEl.remove();
175
+ registeredTabs.delete(id);
176
+ }
177
+
178
+ /**
179
+ * Get all registered tab IDs (including built-in ones managed by this SDK).
180
+ */
181
+ export function getRegisteredTabs() {
182
+ return [...registeredTabs.keys()];
183
+ }
184
+
185
+ // ── Internal ────────────────────────────────────────────
186
+
187
+ function buildCtx(tab) {
188
+ return {
189
+ // Event bus
190
+ on,
191
+ emit,
192
+
193
+ // State
194
+ getState,
195
+ onState,
196
+
197
+ // API
198
+ api,
199
+
200
+ // Convenience
201
+ getProjectPath: () => $.projectSelect?.value || '',
202
+ getSessionId: () => getState('sessionId'),
203
+
204
+ // Tab-specific
205
+ showBadge(count) {
206
+ if (!tab._btnEl) return;
207
+ let badge = tab._badgeEl;
208
+ if (count > 0) {
209
+ if (!badge) {
210
+ badge = document.createElement('span');
211
+ badge.className = 'right-panel-tab-badge';
212
+ tab._btnEl.appendChild(badge);
213
+ tab._badgeEl = badge;
214
+ }
215
+ badge.textContent = count;
216
+ badge.style.display = '';
217
+ } else if (badge) {
218
+ badge.style.display = 'none';
219
+ }
220
+ },
221
+ clearBadge() {
222
+ if (tab._badgeEl) {
223
+ tab._badgeEl.style.display = 'none';
224
+ }
225
+ },
226
+ setTitle(text) {
227
+ if (tab._btnEl) {
228
+ const titleSpan = tab._btnEl.querySelector('.tab-title');
229
+ if (titleSpan) titleSpan.textContent = text;
230
+ else tab._btnEl.childNodes[tab._btnEl.childNodes.length - 1].textContent = text;
231
+ }
232
+ },
233
+ };
234
+ }
235
+
236
+ function mountTab(tab) {
237
+ if (!tabBarEl || !contentEl) return;
238
+
239
+ // Create tab button
240
+ const btn = document.createElement('button');
241
+ btn.className = 'right-panel-tab';
242
+ btn.dataset.tab = tab.id;
243
+
244
+ if (tab.icon) {
245
+ const iconSpan = document.createElement('span');
246
+ iconSpan.className = 'tab-icon';
247
+ iconSpan.innerHTML = tab.icon;
248
+ btn.appendChild(iconSpan);
249
+ }
250
+
251
+ const titleSpan = document.createElement('span');
252
+ titleSpan.className = 'tab-title';
253
+ titleSpan.textContent = tab.title;
254
+ btn.appendChild(titleSpan);
255
+
256
+ // Insert before "+" button (or close button as fallback)
257
+ const addBtn = tabBarEl.querySelector('.right-panel-add-tab');
258
+ const insertBefore = addBtn || closeBtn;
259
+
260
+ if (tab.position != null) {
261
+ const allTabs = tabBarEl.querySelectorAll('.right-panel-tab');
262
+ const target = allTabs[tab.position];
263
+ if (target) {
264
+ tabBarEl.insertBefore(btn, target);
265
+ } else {
266
+ tabBarEl.insertBefore(btn, insertBefore);
267
+ }
268
+ } else {
269
+ tabBarEl.insertBefore(btn, insertBefore);
270
+ }
271
+
272
+ tab._btnEl = btn;
273
+
274
+ // Create pane
275
+ const pane = document.createElement('div');
276
+ pane.className = 'right-panel-pane';
277
+ pane.dataset.tab = tab.id;
278
+ contentEl.appendChild(pane);
279
+ tab._paneEl = pane;
280
+
281
+ // Initialize content (unless lazy)
282
+ if (!tab.lazy) {
283
+ initTabContent(tab);
284
+ }
285
+
286
+ // Click handler — switches to this tab via the right-panel API
287
+ btn.addEventListener('click', () => {
288
+ // Update all tab buttons
289
+ tabBarEl.querySelectorAll('.right-panel-tab').forEach(b => {
290
+ b.classList.toggle('active', b === btn);
291
+ });
292
+
293
+ // Update all panes
294
+ contentEl.parentElement.querySelectorAll('.right-panel-pane').forEach(p => {
295
+ p.classList.toggle('active', p.dataset.tab === tab.id);
296
+ });
297
+
298
+ localStorage.setItem('claudeck-right-panel-tab', tab.id);
299
+ emit('rightPanel:tabChanged', tab.id);
300
+ });
301
+ }
302
+
303
+ function initTabContent(tab) {
304
+ if (tab._initialized) return;
305
+ tab._initialized = true;
306
+
307
+ const ctx = buildCtx(tab);
308
+ const el = tab.init(ctx);
309
+ if (el instanceof HTMLElement) {
310
+ tab._paneEl.appendChild(el);
311
+ }
312
+ }
313
+
314
+ function ensureInit(tab) {
315
+ if (tab.lazy && !tab._initialized) {
316
+ initTabContent(tab);
317
+ }
318
+ }
319
+
320
+ // ── Lifecycle hooks ─────────────────────────────────────
321
+
322
+ function onTabActivated(tabId) {
323
+ for (const [id, tab] of registeredTabs) {
324
+ if (id === tabId) {
325
+ ensureInit(tab);
326
+ if (tab.onActivate) tab.onActivate();
327
+ } else {
328
+ if (tab._initialized && tab.onDeactivate) tab.onDeactivate();
329
+ }
330
+ }
331
+ }
332
+
333
+ // ── Init ────────────────────────────────────────────────
334
+
335
+ export function initTabSDK() {
336
+ const panel = $.rightPanel;
337
+ if (!panel) return;
338
+
339
+ tabBarEl = panel.querySelector('.right-panel-tab-bar');
340
+ contentEl = panel.querySelector('.right-panel-content');
341
+ closeBtn = panel.querySelector('.right-panel-close');
342
+
343
+ if (!tabBarEl || !contentEl) return;
344
+
345
+ initialized = true;
346
+
347
+ // Add "+" button to open Plugin Marketplace (insert before close button)
348
+ const addBtn = document.createElement('button');
349
+ addBtn.className = 'right-panel-add-tab';
350
+ addBtn.title = 'Plugin Marketplace';
351
+ addBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>';
352
+ addBtn.addEventListener('click', () => openMarketplace());
353
+ tabBarEl.insertBefore(addBtn, closeBtn);
354
+
355
+ // Mount any tabs registered before init (inserted before "+" button)
356
+ for (const tab of pendingTabs) {
357
+ mountTab(tab);
358
+ }
359
+ pendingTabs.length = 0;
360
+
361
+ // Apply saved plugin order
362
+ const savedEnabled = getEnabledPluginNames();
363
+ if (savedEnabled.length) reorderPluginTabs(savedEnabled);
364
+
365
+ // Listen for tab changes to fire lifecycle hooks
366
+ on('rightPanel:tabChanged', onTabActivated);
367
+ on('rightPanel:opened', onTabActivated);
368
+ }
369
+
370
+ // ── Plugin Marketplace ──────────────────────────────────
371
+
372
+ function openMarketplace() {
373
+ // Don't open multiple
374
+ if (document.querySelector('.marketplace-overlay')) return;
375
+
376
+ const plugins = getSortedPlugins();
377
+ const enabled = new Set(getEnabledPluginNames());
378
+
379
+ // Overlay
380
+ const overlay = document.createElement('div');
381
+ overlay.className = 'marketplace-overlay';
382
+
383
+ // Popup
384
+ const popup = document.createElement('div');
385
+ popup.className = 'marketplace-popup';
386
+
387
+ // Header
388
+ const header = document.createElement('div');
389
+ header.className = 'marketplace-header';
390
+ header.innerHTML = `
391
+ <h3>Plugin Marketplace</h3>
392
+ <span class="marketplace-subtitle">${plugins.length} plugin${plugins.length !== 1 ? 's' : ''} available · drag to reorder</span>
393
+ `;
394
+ popup.appendChild(header);
395
+
396
+ // Plugin list
397
+ const list = document.createElement('div');
398
+ list.className = 'marketplace-list';
399
+
400
+ if (!plugins.length) {
401
+ list.innerHTML = '<div class="marketplace-empty">No plugins available.<br>Drop files into <code>plugins/</code> to get started.</div>';
402
+ }
403
+
404
+ // Track pending selections (start from current state)
405
+ const pending = new Set(enabled);
406
+
407
+ // ── Drag state ──
408
+ let dragItem = null;
409
+ let dragPlaceholder = null;
410
+
411
+ for (const plugin of plugins) {
412
+ const meta = getPluginMeta(plugin.name);
413
+ const tabId = getPluginTabId(plugin.name);
414
+ const loaded = tabId && registeredTabs.has(tabId);
415
+
416
+ const item = document.createElement('div');
417
+ item.className = 'marketplace-item';
418
+ item.dataset.plugin = plugin.name;
419
+ item.draggable = true;
420
+ if (pending.has(plugin.name)) item.classList.add('selected');
421
+
422
+ item.innerHTML = `
423
+ <div class="marketplace-drag-handle" title="Drag to reorder">⠿</div>
424
+ <div class="marketplace-item-icon">${meta.icon}</div>
425
+ <div class="marketplace-item-info">
426
+ <div class="marketplace-item-name">${formatPluginName(plugin.name)}</div>
427
+ <div class="marketplace-item-desc">${meta.description}</div>
428
+ </div>
429
+ <div class="marketplace-item-status">
430
+ ${loaded ? '<span class="marketplace-loaded">loaded</span>' : ''}
431
+ </div>
432
+ <div class="marketplace-item-toggle">
433
+ <div class="marketplace-checkbox ${pending.has(plugin.name) ? 'checked' : ''}"></div>
434
+ </div>
435
+ `;
436
+
437
+ // Toggle selection (ignore clicks on drag handle)
438
+ item.addEventListener('click', (e) => {
439
+ if (e.target.closest('.marketplace-drag-handle')) return;
440
+ const cb = item.querySelector('.marketplace-checkbox');
441
+ if (pending.has(plugin.name)) {
442
+ pending.delete(plugin.name);
443
+ cb.classList.remove('checked');
444
+ item.classList.remove('selected');
445
+ } else {
446
+ pending.add(plugin.name);
447
+ cb.classList.add('checked');
448
+ item.classList.add('selected');
449
+ }
450
+ });
451
+
452
+ // ── Drag events ──
453
+ item.addEventListener('dragstart', (e) => {
454
+ dragItem = item;
455
+ item.classList.add('dragging');
456
+ e.dataTransfer.effectAllowed = 'move';
457
+
458
+ // Create placeholder
459
+ dragPlaceholder = document.createElement('div');
460
+ dragPlaceholder.className = 'marketplace-drop-indicator';
461
+
462
+ requestAnimationFrame(() => { item.style.opacity = '0.4'; });
463
+ });
464
+
465
+ item.addEventListener('dragend', () => {
466
+ if (dragItem) {
467
+ dragItem.classList.remove('dragging');
468
+ dragItem.style.opacity = '';
469
+ }
470
+ if (dragPlaceholder && dragPlaceholder.parentNode) {
471
+ dragPlaceholder.remove();
472
+ }
473
+ dragItem = null;
474
+ dragPlaceholder = null;
475
+ });
476
+
477
+ item.addEventListener('dragover', (e) => {
478
+ e.preventDefault();
479
+ e.dataTransfer.dropEffect = 'move';
480
+ if (!dragItem || dragItem === item) return;
481
+
482
+ const rect = item.getBoundingClientRect();
483
+ const midY = rect.top + rect.height / 2;
484
+ const after = e.clientY > midY;
485
+
486
+ if (after) {
487
+ item.after(dragPlaceholder);
488
+ } else {
489
+ item.before(dragPlaceholder);
490
+ }
491
+ });
492
+
493
+ item.addEventListener('drop', (e) => {
494
+ e.preventDefault();
495
+ if (!dragItem || dragItem === item) return;
496
+
497
+ // Insert dragged item where the placeholder is
498
+ if (dragPlaceholder && dragPlaceholder.parentNode) {
499
+ dragPlaceholder.before(dragItem);
500
+ dragPlaceholder.remove();
501
+ }
502
+
503
+ dragItem.classList.remove('dragging');
504
+ dragItem.style.opacity = '';
505
+ dragItem = null;
506
+ dragPlaceholder = null;
507
+ });
508
+
509
+ list.appendChild(item);
510
+ }
511
+
512
+ popup.appendChild(list);
513
+
514
+ // Footer with Apply / Cancel
515
+ const footer = document.createElement('div');
516
+ footer.className = 'marketplace-footer';
517
+
518
+ const cancelBtn = document.createElement('button');
519
+ cancelBtn.className = 'marketplace-btn marketplace-btn-cancel';
520
+ cancelBtn.textContent = 'Cancel';
521
+ cancelBtn.addEventListener('click', () => overlay.remove());
522
+
523
+ const applyBtn = document.createElement('button');
524
+ applyBtn.className = 'marketplace-btn marketplace-btn-apply';
525
+ applyBtn.textContent = 'Apply';
526
+ applyBtn.addEventListener('click', async () => {
527
+ // Read order from current DOM positions
528
+ const orderedNames = [...list.querySelectorAll('.marketplace-item')]
529
+ .map(el => el.dataset.plugin)
530
+ .filter(Boolean);
531
+
532
+ setPluginOrder(orderedNames);
533
+
534
+ // Only enabled in the order they appear
535
+ const newEnabled = orderedNames.filter(n => pending.has(n));
536
+ setEnabledPluginNames(newEnabled);
537
+
538
+ // Unload (hide) disabled plugins first
539
+ for (const [id] of [...registeredTabs]) {
540
+ if (!isPluginTab(id)) continue;
541
+ const belongsToAny = newEnabled.some(n => getPluginTabId(n) === id);
542
+ if (!belongsToAny) {
543
+ unregisterTab(id);
544
+ }
545
+ }
546
+
547
+ // Load newly enabled plugins in order
548
+ for (const name of newEnabled) {
549
+ const existingTabId = getPluginTabId(name);
550
+
551
+ if (existingTabId && registeredTabs.has(existingTabId)) continue;
552
+
553
+ if (existingTabId && reRegisterTab(existingTabId)) continue;
554
+
555
+ if (!isPluginLoaded(name)) {
556
+ await loadPluginByName(name);
557
+ }
558
+ }
559
+
560
+ // Reorder tab buttons & panes in the DOM to match marketplace order
561
+ reorderPluginTabs(newEnabled);
562
+
563
+ overlay.remove();
564
+ });
565
+
566
+ footer.appendChild(cancelBtn);
567
+ footer.appendChild(applyBtn);
568
+ popup.appendChild(footer);
569
+
570
+ overlay.appendChild(popup);
571
+ overlay.addEventListener('click', (e) => {
572
+ if (e.target === overlay) overlay.remove();
573
+ });
574
+
575
+ // Close on Escape
576
+ const onKey = (e) => {
577
+ if (e.key === 'Escape') { overlay.remove(); document.removeEventListener('keydown', onKey); }
578
+ };
579
+ document.addEventListener('keydown', onKey);
580
+
581
+ document.body.appendChild(overlay);
582
+ }
583
+
584
+ /**
585
+ * Reorder plugin tab buttons and panes in the DOM to match the given order.
586
+ * Built-in tabs stay in place; plugin tabs are repositioned after them.
587
+ */
588
+ function reorderPluginTabs(enabledNames) {
589
+ if (!tabBarEl || !contentEl) return;
590
+
591
+ const addBtn = tabBarEl.querySelector('.right-panel-add-tab');
592
+ const insertBeforeBtn = addBtn || closeBtn;
593
+
594
+ // Resolve ordered tab IDs from plugin names
595
+ const orderedTabIds = enabledNames
596
+ .map(name => getPluginTabId(name))
597
+ .filter(id => id && registeredTabs.has(id));
598
+
599
+ // Move each plugin tab button (in order) right before the "+" button
600
+ for (const tabId of orderedTabIds) {
601
+ const tab = registeredTabs.get(tabId);
602
+ if (tab?._btnEl) {
603
+ tabBarEl.insertBefore(tab._btnEl, insertBeforeBtn);
604
+ }
605
+ if (tab?._paneEl) {
606
+ contentEl.appendChild(tab._paneEl);
607
+ }
608
+ }
609
+ }
610
+
611
+ /** Built-in (hardcoded) tab IDs that are never managed by the marketplace */
612
+ const BUILTIN_TABS = new Set(['files', 'git', 'mcp', 'tips', 'assistant', 'tab-sdk', 'architecture', 'adding-features']);
613
+
614
+ function isPluginTab(tabId) {
615
+ return !BUILTIN_TABS.has(tabId);
616
+ }
617
+
618
+ function formatPluginName(name) {
619
+ return name
620
+ .replace(/-tab$/, '')
621
+ .replace(/-/g, ' ')
622
+ .replace(/\b\w/g, c => c.toUpperCase());
623
+ }
@@ -0,0 +1,38 @@
1
+ // Dark/Light theme toggle
2
+ import { $ } from '../core/dom.js';
3
+
4
+ export function applyTheme(theme) {
5
+ document.documentElement.setAttribute("data-theme", theme);
6
+ localStorage.setItem("claudeck-theme", theme);
7
+
8
+ // Update icon visibility
9
+ if (theme === "light") {
10
+ $.themeIconSun.style.display = "none";
11
+ $.themeIconMoon.style.display = "block";
12
+ } else {
13
+ $.themeIconSun.style.display = "block";
14
+ $.themeIconMoon.style.display = "none";
15
+ }
16
+
17
+ // Update Mermaid theme
18
+ if (typeof mermaid !== "undefined") {
19
+ mermaid.initialize({ startOnLoad: false, theme: theme === "light" ? "default" : "dark" });
20
+ }
21
+
22
+ // Update highlight.js theme stylesheet
23
+ const hljsLink = document.getElementById("hljs-theme");
24
+ if (hljsLink) {
25
+ hljsLink.href = theme === "light"
26
+ ? "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css"
27
+ : "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css";
28
+ }
29
+ }
30
+
31
+ // Initialize theme from localStorage
32
+ const savedTheme = localStorage.getItem("claudeck-theme") || "dark";
33
+ applyTheme(savedTheme);
34
+
35
+ $.themeToggleBtn.addEventListener("click", () => {
36
+ const current = document.documentElement.getAttribute("data-theme") || "dark";
37
+ applyTheme(current === "dark" ? "light" : "dark");
38
+ });
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "Claudeck",
3
+ "short_name": "Claudeck",
4
+ "description": "Local Web UI for Claude Code",
5
+ "start_url": "/",
6
+ "display": "standalone",
7
+ "background_color": "#0d1117",
8
+ "theme_color": "#0d1117",
9
+ "icons": [
10
+ { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
11
+ { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
12
+ ]
13
+ }