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.
- package/LICENSE +21 -0
- package/README.md +233 -0
- package/cli.js +2 -0
- package/config/agent-chains.json +16 -0
- package/config/agent-dags.json +16 -0
- package/config/agents.json +46 -0
- package/config/bot-prompt.json +3 -0
- package/config/folders.json +66 -0
- package/config/prompts.json +92 -0
- package/config/repos.json +86 -0
- package/config/telegram-config.json +17 -0
- package/config/workflows.json +90 -0
- package/db.js +1198 -0
- package/package.json +55 -0
- package/plugins/claude-editor/client.css +171 -0
- package/plugins/claude-editor/client.js +183 -0
- package/plugins/event-stream/client.css +207 -0
- package/plugins/event-stream/client.js +271 -0
- package/plugins/linear/client.css +345 -0
- package/plugins/linear/client.js +380 -0
- package/plugins/linear/config.json +5 -0
- package/plugins/linear/server.js +312 -0
- package/plugins/repos/client.css +549 -0
- package/plugins/repos/client.js +663 -0
- package/plugins/repos/server.js +232 -0
- package/plugins/sudoku/client.css +196 -0
- package/plugins/sudoku/client.js +329 -0
- package/plugins/tasks/client.css +414 -0
- package/plugins/tasks/client.js +394 -0
- package/plugins/tasks/server.js +116 -0
- package/plugins/tic-tac-toe/client.css +167 -0
- package/plugins/tic-tac-toe/client.js +241 -0
- package/public/css/core/components.css +232 -0
- package/public/css/core/layout.css +330 -0
- package/public/css/core/print.css +18 -0
- package/public/css/core/reset.css +36 -0
- package/public/css/core/responsive.css +378 -0
- package/public/css/core/theme.css +116 -0
- package/public/css/core/variables.css +93 -0
- package/public/css/features/agent-monitor.css +297 -0
- package/public/css/features/agent-sidebar.css +525 -0
- package/public/css/features/agents.css +996 -0
- package/public/css/features/analytics.css +181 -0
- package/public/css/features/background-sessions.css +321 -0
- package/public/css/features/cost-dashboard.css +168 -0
- package/public/css/features/home.css +313 -0
- package/public/css/features/retro-terminal.css +88 -0
- package/public/css/features/telegram.css +127 -0
- package/public/css/features/tour.css +148 -0
- package/public/css/features/voice-input.css +60 -0
- package/public/css/features/welcome.css +241 -0
- package/public/css/panels/assistant-bot.css +442 -0
- package/public/css/panels/dev-docs.css +292 -0
- package/public/css/panels/file-explorer.css +322 -0
- package/public/css/panels/git-panel.css +221 -0
- package/public/css/panels/mcp-manager.css +199 -0
- package/public/css/panels/tips-feed.css +353 -0
- package/public/css/ui/commands.css +273 -0
- package/public/css/ui/context-gauge.css +76 -0
- package/public/css/ui/file-picker.css +69 -0
- package/public/css/ui/image-attachments.css +106 -0
- package/public/css/ui/messages.css +884 -0
- package/public/css/ui/modals.css +122 -0
- package/public/css/ui/parallel.css +217 -0
- package/public/css/ui/permissions.css +110 -0
- package/public/css/ui/right-panel.css +481 -0
- package/public/css/ui/sessions.css +689 -0
- package/public/css/ui/status-bar.css +425 -0
- package/public/css/ui/toolbox.css +206 -0
- package/public/data/tips.json +218 -0
- package/public/icons/favicon.png +0 -0
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-512.png +0 -0
- package/public/icons/whaly.png +0 -0
- package/public/index.html +1140 -0
- package/public/js/core/api.js +591 -0
- package/public/js/core/constants.js +3 -0
- package/public/js/core/dom.js +270 -0
- package/public/js/core/events.js +10 -0
- package/public/js/core/plugin-loader.js +153 -0
- package/public/js/core/store.js +39 -0
- package/public/js/core/utils.js +25 -0
- package/public/js/core/ws.js +64 -0
- package/public/js/features/agent-monitor.js +222 -0
- package/public/js/features/agents.js +1209 -0
- package/public/js/features/analytics.js +397 -0
- package/public/js/features/attachments.js +251 -0
- package/public/js/features/background-sessions.js +475 -0
- package/public/js/features/chat.js +589 -0
- package/public/js/features/cost-dashboard.js +152 -0
- package/public/js/features/dag-editor.js +399 -0
- package/public/js/features/easter-egg.js +46 -0
- package/public/js/features/home.js +270 -0
- package/public/js/features/projects.js +372 -0
- package/public/js/features/prompts.js +228 -0
- package/public/js/features/sessions.js +332 -0
- package/public/js/features/telegram.js +131 -0
- package/public/js/features/tour.js +210 -0
- package/public/js/features/voice-input.js +185 -0
- package/public/js/features/welcome.js +43 -0
- package/public/js/features/workflows.js +277 -0
- package/public/js/main.js +51 -0
- package/public/js/panels/assistant-bot.js +445 -0
- package/public/js/panels/dev-docs.js +380 -0
- package/public/js/panels/file-explorer.js +486 -0
- package/public/js/panels/git-panel.js +285 -0
- package/public/js/panels/mcp-manager.js +311 -0
- package/public/js/panels/tips-feed.js +303 -0
- package/public/js/ui/commands.js +114 -0
- package/public/js/ui/context-gauge.js +100 -0
- package/public/js/ui/diff.js +124 -0
- package/public/js/ui/disabled-tools.js +36 -0
- package/public/js/ui/export.js +74 -0
- package/public/js/ui/formatting.js +206 -0
- package/public/js/ui/header-dropdowns.js +72 -0
- package/public/js/ui/input-meta.js +71 -0
- package/public/js/ui/max-turns.js +21 -0
- package/public/js/ui/messages.js +387 -0
- package/public/js/ui/model-selector.js +20 -0
- package/public/js/ui/notifications.js +232 -0
- package/public/js/ui/parallel.js +176 -0
- package/public/js/ui/permissions.js +168 -0
- package/public/js/ui/right-panel.js +173 -0
- package/public/js/ui/shortcuts.js +143 -0
- package/public/js/ui/sidebar-toggle.js +29 -0
- package/public/js/ui/status-bar.js +172 -0
- package/public/js/ui/tab-sdk.js +623 -0
- package/public/js/ui/theme.js +38 -0
- package/public/manifest.json +13 -0
- package/public/offline.html +190 -0
- package/public/style.css +42 -0
- package/public/sw.js +91 -0
- package/server/agent-loop.js +385 -0
- package/server/dag-executor.js +265 -0
- package/server/orchestrator.js +514 -0
- package/server/paths.js +61 -0
- package/server/plugin-mount.js +56 -0
- package/server/push-sender.js +31 -0
- package/server/routes/agents.js +294 -0
- package/server/routes/bot.js +45 -0
- package/server/routes/exec.js +35 -0
- package/server/routes/files.js +218 -0
- package/server/routes/mcp.js +82 -0
- package/server/routes/messages.js +36 -0
- package/server/routes/notifications.js +37 -0
- package/server/routes/projects.js +207 -0
- package/server/routes/prompts.js +53 -0
- package/server/routes/sessions.js +103 -0
- package/server/routes/stats.js +143 -0
- package/server/routes/telegram.js +71 -0
- package/server/routes/tips.js +135 -0
- package/server/routes/workflows.js +81 -0
- package/server/summarizer.js +55 -0
- package/server/telegram-poller.js +205 -0
- package/server/telegram-sender.js +304 -0
- package/server/ws-handler.js +926 -0
- 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
|
+
}
|