claudeck 1.4.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +6 -8
  2. package/package.json +1 -1
  3. package/plugins/claude-editor/manifest.json +10 -0
  4. package/plugins/repos/manifest.json +10 -0
  5. package/public/css/core/theme.css +6 -21
  6. package/public/css/core/variables.css +2 -0
  7. package/public/css/features/message-queue.css +348 -0
  8. package/public/css/ui/commands.css +4 -4
  9. package/public/css/ui/messages.css +310 -78
  10. package/public/css/ui/right-panel.css +207 -0
  11. package/public/css/ui/sessions.css +173 -0
  12. package/public/css/ui/settings.css +75 -0
  13. package/public/index.html +10 -2
  14. package/public/js/components/add-project-modal.js +14 -0
  15. package/public/js/components/jump-to-latest.js +42 -0
  16. package/public/js/components/queue-stop-modal.js +23 -0
  17. package/public/js/components/settings-modal.js +65 -0
  18. package/public/js/core/api.js +15 -43
  19. package/public/js/core/dom.js +17 -0
  20. package/public/js/core/events.js +11 -0
  21. package/public/js/core/plugin-loader.js +96 -11
  22. package/public/js/core/store.js +11 -0
  23. package/public/js/core/utils.js +38 -2
  24. package/public/js/features/chat.js +49 -1
  25. package/public/js/features/message-queue.js +423 -0
  26. package/public/js/features/projects.js +185 -3
  27. package/public/js/main.js +4 -1
  28. package/public/js/panels/assistant-bot.js +16 -0
  29. package/public/js/panels/dev-docs.js +2 -2
  30. package/public/js/panels/memory.js +1 -0
  31. package/public/js/ui/context-gauge.js +10 -1
  32. package/public/js/ui/formatting.js +65 -11
  33. package/public/js/ui/header-dropdowns.js +30 -0
  34. package/public/js/ui/input-meta.js +13 -6
  35. package/public/js/ui/max-turns.js +6 -3
  36. package/public/js/ui/messages.js +97 -1
  37. package/public/js/ui/model-selector.js +1 -0
  38. package/public/js/ui/parallel.js +32 -2
  39. package/public/js/ui/permissions.js +1 -0
  40. package/public/js/ui/right-panel.js +0 -8
  41. package/public/js/ui/tab-sdk.js +395 -176
  42. package/public/style.css +2 -0
  43. package/server/memory-optimizer.js +17 -13
  44. package/server/routes/marketplace.js +316 -0
  45. package/server/routes/projects.js +0 -0
  46. package/server/ws-handler.js +22 -15
  47. package/server.js +18 -0
  48. package/plugins/event-stream/client.css +0 -207
  49. package/plugins/event-stream/client.js +0 -271
  50. package/plugins/linear/client.css +0 -345
  51. package/plugins/linear/client.js +0 -380
  52. package/plugins/linear/config.json +0 -5
  53. package/plugins/linear/server.js +0 -312
  54. package/plugins/sudoku/client.css +0 -196
  55. package/plugins/sudoku/client.js +0 -329
  56. package/plugins/tasks/client.css +0 -414
  57. package/plugins/tasks/client.js +0 -394
  58. package/plugins/tasks/server.js +0 -116
  59. package/plugins/tic-tac-toe/client.css +0 -167
  60. package/plugins/tic-tac-toe/client.js +0 -241
  61. package/public/js/components/linear-create-modal.js +0 -43
@@ -310,6 +310,21 @@ export async function addProject(name, path) {
310
310
  return res.json();
311
311
  }
312
312
 
313
+ export async function createFolder(parent, name) {
314
+ const res = await fetch("/api/projects/create-folder", {
315
+ method: "POST",
316
+ headers: { "Content-Type": "application/json" },
317
+ body: JSON.stringify({ parent, name }),
318
+ });
319
+ const body = await res.json().catch(() => ({}));
320
+ if (!res.ok) {
321
+ const err = new Error(body.error || "Failed to create folder");
322
+ err.status = res.status;
323
+ throw err;
324
+ }
325
+ return body;
326
+ }
327
+
313
328
  export async function deleteProject(path) {
314
329
  const res = await fetch("/api/projects", {
315
330
  method: "DELETE",
@@ -474,49 +489,6 @@ export async function execCommand(command, cwd) {
474
489
  return res.json();
475
490
  }
476
491
 
477
- export async function fetchLinearIssues() {
478
- const res = await fetch("/api/plugins/linear/issues");
479
- return res.json();
480
- }
481
-
482
- export async function fetchLinearTeams() {
483
- const res = await fetch("/api/plugins/linear/teams");
484
- return res.json();
485
- }
486
-
487
- export async function fetchLinearTeamStates(teamId) {
488
- const res = await fetch(`/api/plugins/linear/teams/${encodeURIComponent(teamId)}/states`);
489
- return res.json();
490
- }
491
-
492
- export async function createLinearIssue({ title, description, teamId, stateId }) {
493
- const res = await fetch("/api/plugins/linear/issues", {
494
- method: "POST",
495
- headers: { "Content-Type": "application/json" },
496
- body: JSON.stringify({ title, description, teamId, stateId }),
497
- });
498
- if (!res.ok) throw new Error("Failed to create issue");
499
- return res.json();
500
- }
501
-
502
- export async function fetchLinearConfig() {
503
- const res = await fetch("/api/plugins/linear/config");
504
- return res.json();
505
- }
506
-
507
- export async function saveLinearConfig(config) {
508
- const res = await fetch("/api/plugins/linear/config", {
509
- method: "PUT",
510
- headers: { "Content-Type": "application/json" },
511
- body: JSON.stringify(config),
512
- });
513
- return res.json();
514
- }
515
-
516
- export async function testLinearConnection() {
517
- const res = await fetch("/api/plugins/linear/test", { method: "POST" });
518
- return res.json();
519
- }
520
492
 
521
493
  // Tips
522
494
  export async function fetchTips() {
@@ -165,6 +165,13 @@ export const $ = {
165
165
  bgSessionIndicator: document.getElementById("bg-session-indicator"),
166
166
  bgSessionBadge: document.getElementById("bg-session-badge"),
167
167
 
168
+ // Message queue
169
+ queueStopModal: document.getElementById("queue-stop-modal"),
170
+ queueStopAll: document.getElementById("queue-stop-all"),
171
+ queueStopSkip: document.getElementById("queue-stop-skip"),
172
+ queueStopPause: document.getElementById("queue-stop-pause"),
173
+ queueStopPreview: document.getElementById("queue-stop-preview"),
174
+
168
175
  // Telegram
169
176
  telegramBtn: document.getElementById("telegram-settings-btn"),
170
177
  telegramModal: document.getElementById("telegram-modal"),
@@ -293,5 +300,15 @@ export const $ = {
293
300
  addProjectConfirm: document.getElementById("add-project-confirm"),
294
301
  folderBreadcrumb: document.getElementById("folder-breadcrumb"),
295
302
  folderList: document.getElementById("folder-list"),
303
+ folderPathInput: document.getElementById("folder-path-input"),
304
+ folderPathGo: document.getElementById("folder-path-go"),
305
+ folderRecents: document.getElementById("folder-recents"),
306
+ folderNewToggle: document.getElementById("folder-new-toggle"),
307
+ folderNewToggleRow: document.getElementById("folder-new-toggle-row"),
308
+ folderNewRow: document.getElementById("folder-new-row"),
309
+ folderNewName: document.getElementById("folder-new-name"),
310
+ folderNewCreate: document.getElementById("folder-new-create"),
311
+ folderNewCancel: document.getElementById("folder-new-cancel"),
312
+ addProjectMessage: document.getElementById("add-project-message"),
296
313
 
297
314
  };
@@ -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) {
@@ -20,6 +20,42 @@ export function getToolDetail(name, input) {
20
20
  return "";
21
21
  }
22
22
 
23
- export function scrollToBottom(pane) {
24
- pane.messagesDiv.scrollTop = pane.messagesDiv.scrollHeight;
23
+ // Distance from bottom (px) within which we still consider the user "at bottom".
24
+ // Absorbs sub-pixel rounding, momentum scrolling, and small layout shifts.
25
+ export const NEAR_BOTTOM_THRESHOLD = 100;
26
+
27
+ export function isNearBottom(el) {
28
+ if (!el) return true;
29
+ return el.scrollHeight - el.scrollTop - el.clientHeight < NEAR_BOTTOM_THRESHOLD;
30
+ }
31
+
32
+ // Stick-to-bottom: only scrolls if the user is already near the bottom, or
33
+ // if `force` is set (e.g. user sent a message). When new content arrives
34
+ // while the user has scrolled up, we set `pane.hasNewBelow = true` so the
35
+ // "Jump to latest" pill can surface, and we leave their scroll position alone.
36
+ export function scrollToBottom(pane, opts = {}) {
37
+ if (!pane || !pane.messagesDiv) return;
38
+ const el = pane.messagesDiv;
39
+ if (opts.force) {
40
+ el.scrollTop = el.scrollHeight;
41
+ pane.followBottom = true;
42
+ pane.hasNewBelow = false;
43
+ notifyPillUpdate(pane);
44
+ return;
45
+ }
46
+ if (pane.followBottom !== false && isNearBottom(el)) {
47
+ el.scrollTop = el.scrollHeight;
48
+ pane.followBottom = true;
49
+ pane.hasNewBelow = false;
50
+ } else {
51
+ pane.hasNewBelow = true;
52
+ }
53
+ notifyPillUpdate(pane);
54
+ }
55
+
56
+ function notifyPillUpdate(pane) {
57
+ if (typeof window === "undefined") return;
58
+ window.dispatchEvent(new CustomEvent("claudeck:scroll-state", {
59
+ detail: { chatId: pane.chatId, hasNewBelow: !!pane.hasNewBelow },
60
+ }));
25
61
  }
@@ -4,7 +4,8 @@ import { getState, setState } from '../core/store.js';
4
4
  import { CHAT_IDS, BOT_CHAT_ID } from '../core/constants.js';
5
5
  import { on } from '../core/events.js';
6
6
  import { commandRegistry, dismissAutocomplete, handleAutocompleteKeydown, handleSlashAutocomplete, registerCommand } from '../ui/commands.js';
7
- import { addUserMessage, appendAssistantText, appendToolIndicator, appendToolResult, showThinking, removeThinking, addResultSummary, addStatus, showWhalyPlaceholder, addSkillUsedMessage } from '../ui/messages.js';
7
+ import { addUserMessage, appendAssistantText, appendToolIndicator, appendToolResult, showThinking, removeThinking, addResultSummary, addStatus, showWhalyPlaceholder, addSkillUsedMessage, exitWelcomeState, isWelcomeStateActive } from '../ui/messages.js';
8
+ import { enqueueMessage, pauseQueue, resumeQueue, fireNextQueued, handleStopWithQueue } from './message-queue.js';
8
9
  import { getPane, panes, _setChatFns, _setInputHistoryGetter } from '../ui/parallel.js';
9
10
  import { loadSessions } from './sessions.js';
10
11
  import { loadStats, loadAccountInfo } from './cost-dashboard.js';
@@ -100,6 +101,15 @@ export function sendMessage(pane) {
100
101
  const text = pane.messageInput.value.trim();
101
102
  const cwd = $.projectSelect.value;
102
103
 
104
+ // Queue message if currently streaming (don't queue slash commands)
105
+ if (pane.isStreaming && text && !text.startsWith('/')) {
106
+ enqueueMessage(text, pane);
107
+ pane.messageInput.value = "";
108
+ pane.messageInput.style.height = "auto";
109
+ dismissAutocomplete(pane);
110
+ return;
111
+ }
112
+
103
113
  if (!text || !cwd) {
104
114
  if (text && text.startsWith("/")) {
105
115
  const match = text.match(/^\/(\S+)\s*(.*)/s);
@@ -155,6 +165,24 @@ export function sendMessage(pane) {
155
165
  }
156
166
  }
157
167
 
168
+ // Resume queue if responding to a question
169
+ if (pane._queuePaused && pane._queuePauseReason === 'question') {
170
+ resumeQueue(pane);
171
+ }
172
+
173
+ // Animate out of welcome state if active
174
+ if (isWelcomeStateActive()) {
175
+ exitWelcomeState().then(() => {
176
+ _doSend(text, pane);
177
+ });
178
+ return;
179
+ }
180
+ _doSend(text, pane);
181
+ }
182
+
183
+ export function _doSend(text, pane) {
184
+ const cwd = $.projectSelect.value;
185
+ const ws = getState("ws");
158
186
  // Prepend attached files
159
187
  let fullMessage = text;
160
188
  const attachedFiles = getState("attachedFiles");
@@ -235,6 +263,11 @@ export function sendMessage(pane) {
235
263
 
236
264
  export function stopGeneration(pane) {
237
265
  pane = pane || getPane(null);
266
+ // Show 3-option dialog if queue has items
267
+ if (pane._messageQueue?.length > 0) {
268
+ handleStopWithQueue(pane);
269
+ return;
270
+ }
238
271
  const ws = getState("ws");
239
272
  if (ws && ws.readyState === WebSocket.OPEN) {
240
273
  const payload = { type: "abort" };
@@ -276,6 +309,13 @@ export function finishStreamingHandler(pane) {
276
309
  if (sid) {
277
310
  import('./sessions.js').then(({ loadMessages }) => loadMessages(sid));
278
311
  }
312
+
313
+ // Auto-fire next queued message (deferred to let state settle)
314
+ queueMicrotask(() => {
315
+ if (pane._messageQueue?.length > 0 && !pane._queuePaused) {
316
+ fireNextQueued(pane);
317
+ }
318
+ });
279
319
  }
280
320
 
281
321
  // Register the chat functions with parallel.js to break circular dependency
@@ -443,6 +483,10 @@ function handleServerMessage(msg) {
443
483
  finishStreamingHandler(pane);
444
484
  if (isQuestionText(rawText)) {
445
485
  showWaitingForInput(pane);
486
+ // Pause queue when Claude asks a question
487
+ if (pane._messageQueue?.length > 0) {
488
+ pauseQueue(pane, 'question');
489
+ }
446
490
  }
447
491
  break;
448
492
  }
@@ -455,6 +499,10 @@ function handleServerMessage(msg) {
455
499
  case "error":
456
500
  finishStreamingHandler(pane);
457
501
  addStatus("Error: " + msg.error, true, pane);
502
+ // Pause queue on error
503
+ if (pane._messageQueue?.length > 0) {
504
+ pauseQueue(pane, 'error');
505
+ }
458
506
  break;
459
507
 
460
508
  case "workflow_started":