claudeck 1.3.1 → 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 (60) hide show
  1. package/README.md +13 -9
  2. package/db/sqlite.js +1697 -0
  3. package/db.js +3 -1645
  4. package/package.json +2 -1
  5. package/plugins/claude-editor/manifest.json +10 -0
  6. package/plugins/linear/manifest.json +10 -0
  7. package/plugins/repos/manifest.json +10 -0
  8. package/public/css/ui/messages.css +25 -0
  9. package/public/css/ui/right-panel.css +207 -0
  10. package/public/css/ui/settings.css +75 -0
  11. package/public/index.html +7 -0
  12. package/public/js/components/settings-modal.js +65 -0
  13. package/public/js/core/api.js +23 -6
  14. package/public/js/core/events.js +11 -0
  15. package/public/js/core/plugin-loader.js +96 -11
  16. package/public/js/core/store.js +11 -0
  17. package/public/js/core/ws.js +12 -0
  18. package/public/js/features/chat.js +4 -0
  19. package/public/js/features/sessions.js +102 -10
  20. package/public/js/main.js +1 -0
  21. package/public/js/panels/assistant-bot.js +16 -0
  22. package/public/js/panels/dev-docs.js +2 -2
  23. package/public/js/panels/memory.js +1 -0
  24. package/public/js/ui/context-gauge.js +10 -1
  25. package/public/js/ui/header-dropdowns.js +30 -0
  26. package/public/js/ui/input-meta.js +13 -6
  27. package/public/js/ui/max-turns.js +6 -3
  28. package/public/js/ui/messages.js +42 -0
  29. package/public/js/ui/model-selector.js +1 -0
  30. package/public/js/ui/parallel.js +2 -4
  31. package/public/js/ui/permissions.js +1 -0
  32. package/public/js/ui/tab-sdk.js +395 -176
  33. package/public/style.css +1 -0
  34. package/server/agent-loop.js +26 -26
  35. package/server/memory-extractor.js +4 -4
  36. package/server/memory-injector.js +11 -11
  37. package/server/memory-optimizer.js +19 -15
  38. package/server/notification-logger.js +5 -5
  39. package/server/orchestrator.js +15 -15
  40. package/server/push-sender.js +2 -2
  41. package/server/routes/agents.js +2 -2
  42. package/server/routes/marketplace.js +316 -0
  43. package/server/routes/memory.js +20 -20
  44. package/server/routes/messages.js +41 -10
  45. package/server/routes/notifications.js +20 -20
  46. package/server/routes/sessions.js +17 -17
  47. package/server/routes/stats.js +37 -37
  48. package/server/routes/worktrees.js +9 -9
  49. package/server/summarizer.js +3 -3
  50. package/server/ws-handler.js +163 -58
  51. package/server.js +20 -2
  52. package/plugins/event-stream/client.css +0 -207
  53. package/plugins/event-stream/client.js +0 -271
  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
@@ -399,6 +399,10 @@ function handleServerMessage(msg) {
399
399
  showThinking("Thinking...", pane);
400
400
  break;
401
401
 
402
+ case "user_message":
403
+ addUserMessage(msg.text, pane);
404
+ break;
405
+
402
406
  case "text":
403
407
  appendAssistantText(msg.text, pane);
404
408
  break;
@@ -5,8 +5,12 @@ import { CHAT_IDS } from '../core/constants.js';
5
5
  import { escapeHtml } from '../core/utils.js';
6
6
  import * as api from '../core/api.js';
7
7
  import { panes, enterParallelMode, exitParallelMode } from '../ui/parallel.js';
8
- import { renderMessagesIntoPane, showWhalyPlaceholder } from '../ui/messages.js';
8
+ import { renderMessagesIntoPane, prependOlderMessages, showWhalyPlaceholder, showLoadingIndicator, hideLoadingIndicator } from '../ui/messages.js';
9
9
  import { loadContextGauge } from '../ui/context-gauge.js';
10
+ import { subscribeToSession } from '../core/ws.js';
11
+
12
+ const MESSAGE_PAGE_SIZE = 30;
13
+ const SCROLL_LOAD_THRESHOLD = 150; // px from top to trigger load more
10
14
 
11
15
  const SESSION_STORAGE_KEY = "claudeck-session-id";
12
16
 
@@ -14,6 +18,7 @@ const SESSION_STORAGE_KEY = "claudeck-session-id";
14
18
  onState("sessionId", (val) => {
15
19
  if (val) {
16
20
  localStorage.setItem(SESSION_STORAGE_KEY, val);
21
+ subscribeToSession(val);
17
22
  } else {
18
23
  localStorage.removeItem(SESSION_STORAGE_KEY);
19
24
  }
@@ -228,16 +233,16 @@ export async function deleteSession(id) {
228
233
 
229
234
  export async function loadMessages(sid) {
230
235
  if (getState("parallelMode")) {
231
- for (const chatId of CHAT_IDS) {
232
- loadPaneMessages(sid, chatId);
233
- }
236
+ // Load all panes concurrently instead of sequentially
237
+ await Promise.all(CHAT_IDS.map(chatId => loadPaneMessages(sid, chatId)));
234
238
  return;
235
239
  }
236
240
 
237
241
  const pane = panes.get(null);
238
242
  try {
239
- const messages = await api.fetchSingleMessages(sid);
243
+ const messages = await api.fetchSingleMessages(sid, { limit: MESSAGE_PAGE_SIZE });
240
244
  renderMessagesIntoPane(messages, pane);
245
+ _initPanePagination(pane, messages, "single");
241
246
  loadContextGauge(sid);
242
247
  } catch (err) {
243
248
  console.error("Failed to load messages:", err);
@@ -248,22 +253,109 @@ export async function loadPaneMessages(sid, chatId) {
248
253
  const pane = panes.get(chatId);
249
254
  if (!pane) return;
250
255
  try {
251
- let messages = await api.fetchMessagesByChatId(sid, chatId);
252
-
256
+ let messages;
253
257
  // For Chat 1 (chat-0): also load single-mode messages as fallback
254
258
  if (chatId === CHAT_IDS[0]) {
255
- const singleMessages = await api.fetchSingleMessages(sid);
256
- if (singleMessages.length > 0) {
257
- messages = [...singleMessages, ...messages].sort((a, b) => a.id - b.id);
259
+ const [chatMsgs, singleMsgs] = await Promise.all([
260
+ api.fetchMessagesByChatId(sid, chatId, { limit: MESSAGE_PAGE_SIZE }),
261
+ api.fetchSingleMessages(sid, { limit: MESSAGE_PAGE_SIZE }),
262
+ ]);
263
+ if (singleMsgs.length > 0) {
264
+ messages = [...singleMsgs, ...chatMsgs].sort((a, b) => a.id - b.id);
265
+ } else {
266
+ messages = chatMsgs;
258
267
  }
268
+ } else {
269
+ messages = await api.fetchMessagesByChatId(sid, chatId, { limit: MESSAGE_PAGE_SIZE });
259
270
  }
260
271
 
261
272
  renderMessagesIntoPane(messages, pane);
273
+ _initPanePagination(pane, messages, chatId === CHAT_IDS[0] ? "chat0" : "chat");
262
274
  } catch (err) {
263
275
  console.error(`Failed to load messages for ${chatId}:`, err);
264
276
  }
265
277
  }
266
278
 
279
+ // ── Lazy-load pagination ────────────────────────────────
280
+
281
+ function _initPanePagination(pane, messages, mode) {
282
+ pane._hasMore = messages.length >= MESSAGE_PAGE_SIZE;
283
+ pane._oldestMessageId = messages.length > 0 ? messages[0].id : null;
284
+ pane._loadingMore = false;
285
+ pane._paginationMode = mode; // "single" | "chat" | "chat0"
286
+
287
+ // Remove any existing scroll listener
288
+ if (pane._scrollHandler) {
289
+ pane.messagesDiv.removeEventListener("scroll", pane._scrollHandler);
290
+ }
291
+
292
+ if (pane._hasMore) {
293
+ pane._scrollHandler = () => _onPaneScroll(pane);
294
+ pane.messagesDiv.addEventListener("scroll", pane._scrollHandler, { passive: true });
295
+ }
296
+ }
297
+
298
+ function _onPaneScroll(pane) {
299
+ if (
300
+ pane.messagesDiv.scrollTop < SCROLL_LOAD_THRESHOLD &&
301
+ pane._hasMore &&
302
+ !pane._loadingMore
303
+ ) {
304
+ _loadMoreMessages(pane);
305
+ }
306
+ }
307
+
308
+ async function _loadMoreMessages(pane) {
309
+ pane._loadingMore = true;
310
+ showLoadingIndicator(pane);
311
+
312
+ const sid = getState("sessionId");
313
+ const before = pane._oldestMessageId;
314
+ const opts = { limit: MESSAGE_PAGE_SIZE, before };
315
+
316
+ try {
317
+ let olderMessages;
318
+
319
+ switch (pane._paginationMode) {
320
+ case "single":
321
+ olderMessages = await api.fetchSingleMessages(sid, opts);
322
+ break;
323
+ case "chat0": {
324
+ // Chat 1 merges chatId + single messages
325
+ const [chatMsgs, singleMsgs] = await Promise.all([
326
+ api.fetchMessagesByChatId(sid, pane.chatId, opts),
327
+ api.fetchSingleMessages(sid, opts),
328
+ ]);
329
+ olderMessages = singleMsgs.length > 0
330
+ ? [...singleMsgs, ...chatMsgs].sort((a, b) => a.id - b.id)
331
+ : chatMsgs;
332
+ break;
333
+ }
334
+ default:
335
+ olderMessages = await api.fetchMessagesByChatId(sid, pane.chatId, opts);
336
+ }
337
+
338
+ if (olderMessages.length === 0) {
339
+ pane._hasMore = false;
340
+ } else {
341
+ pane._oldestMessageId = olderMessages[0].id;
342
+ pane._hasMore = olderMessages.length >= MESSAGE_PAGE_SIZE;
343
+ prependOlderMessages(olderMessages, pane);
344
+ }
345
+ } catch (err) {
346
+ console.error("Failed to load more messages:", err);
347
+ } finally {
348
+ hideLoadingIndicator(pane);
349
+ pane._loadingMore = false;
350
+
351
+ // Detach scroll listener if no more messages
352
+ if (!pane._hasMore && pane._scrollHandler) {
353
+ pane.messagesDiv.removeEventListener("scroll", pane._scrollHandler);
354
+ pane._scrollHandler = null;
355
+ }
356
+ }
357
+ }
358
+
267
359
  // ── Session Context Menu ────────────────────────────────
268
360
  let sessionCtxMenu = null;
269
361
 
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
 
@@ -109,6 +109,7 @@ function renderMemories() {
109
109
  try {
110
110
  await fetchApi(`/${m.id}`, { method: 'DELETE' });
111
111
  memories = memories.filter(x => x.id !== m.id);
112
+ if ($.memoryTitle) $.memoryTitle.textContent = `Memory (${memories.length})`;
112
113
  renderMemories();
113
114
  loadStats();
114
115
  } catch { /* ignore */ }
@@ -5,11 +5,13 @@ import { $ } from '../core/dom.js';
5
5
  const sbGaugeSep = document.getElementById("sb-gauge-sep");
6
6
 
7
7
  const MODEL_LIMITS = {
8
+ opus: 1_000_000,
8
9
  default: 200_000,
9
10
  };
10
11
 
11
12
  function getLimit() {
12
- return MODEL_LIMITS.default;
13
+ const model = $.modelSelect?.value || '';
14
+ return MODEL_LIMITS[model] || MODEL_LIMITS.default;
13
15
  }
14
16
 
15
17
  function formatTokens(n) {
@@ -73,6 +75,13 @@ export function resetContextGauge() {
73
75
  if (sbGaugeSep) sbGaugeSep.classList.add('hidden');
74
76
  }
75
77
 
78
+ // Re-render gauge when model changes (limit may differ)
79
+ $.modelSelect?.addEventListener('change', () => {
80
+ const tokens = getState('sessionTokens');
81
+ const total = tokens.input + tokens.output + tokens.cacheRead + tokens.cacheCreation;
82
+ if (total > 0) renderGauge(tokens);
83
+ });
84
+
76
85
  export async function loadContextGauge(sessionId) {
77
86
  if (!sessionId) return;
78
87
  try {
@@ -70,3 +70,33 @@ document.addEventListener("keydown", (e) => {
70
70
  document.querySelectorAll(".header-dropdown.open").forEach((d) => d.classList.remove("open"));
71
71
  }
72
72
  });
73
+
74
+ // Sync header dropdown display when hidden selects change programmatically
75
+ function syncDropdownDisplay(selectId) {
76
+ const select = document.getElementById(selectId);
77
+ if (!select) return;
78
+
79
+ function sync() {
80
+ const val = select.value;
81
+ const items = document.querySelectorAll(`.header-submenu-item[data-target="${selectId}"]`);
82
+ let matchedText = null;
83
+ items.forEach((item) => {
84
+ const isMatch = item.dataset.value === val;
85
+ item.classList.toggle("active", isMatch);
86
+ if (isMatch) matchedText = item.textContent.trim();
87
+ });
88
+ if (matchedText) {
89
+ const parent = items[0]?.closest(".header-dropdown-item");
90
+ const display = parent?.querySelector(".header-dropdown-item-value");
91
+ if (display) display.textContent = matchedText;
92
+ }
93
+ }
94
+
95
+ select.addEventListener("change", sync);
96
+ // Initial sync for values restored from localStorage
97
+ sync();
98
+ }
99
+
100
+ syncDropdownDisplay("model-select");
101
+ syncDropdownDisplay("perm-mode-select");
102
+ syncDropdownDisplay("max-turns-select");
@@ -6,16 +6,23 @@ const elPerm = document.getElementById("input-meta-perm");
6
6
  const elTurns = document.getElementById("input-meta-turns");
7
7
 
8
8
  const permLabels = {
9
- bypass: "bypass",
10
- confirmDangerous: "confirm dangerous",
11
- confirmAll: "confirm all",
12
- plan: "plan only",
9
+ bypass: "Bypass",
10
+ confirmDangerous: "Confirm Writes",
11
+ confirmAll: "Confirm All",
12
+ plan: "Plan Mode",
13
+ };
14
+
15
+ const modelLabels = {
16
+ "": "Auto",
17
+ sonnet: "Sonnet",
18
+ opus: "Opus",
19
+ haiku: "Haiku",
13
20
  };
14
21
 
15
22
  function updateModel() {
16
23
  if (!elModel) return;
17
24
  const val = $.modelSelect?.value || "";
18
- elModel.textContent = val || "default model";
25
+ elModel.textContent = modelLabels[val] || val || "Auto";
19
26
  }
20
27
 
21
28
  function updatePerm() {
@@ -27,7 +34,7 @@ function updatePerm() {
27
34
  function updateTurns() {
28
35
  if (!elTurns) return;
29
36
  const val = $.maxTurnsSelect?.value || "30";
30
- elTurns.textContent = val === "0" ? "unlimited turns" : `${val} turns`;
37
+ elTurns.textContent = val === "0" ? "Unlimited" : `${val} turns`;
31
38
  }
32
39
 
33
40
  // Also watch header dropdown display elements (used when dropdowns replace <select>)
@@ -9,13 +9,16 @@ export function getMaxTurns() {
9
9
  }
10
10
 
11
11
  function init() {
12
+ $.maxTurnsSelect?.addEventListener('change', () => {
13
+ localStorage.setItem(STORAGE_KEY, $.maxTurnsSelect.value);
14
+ });
12
15
  const saved = localStorage.getItem(STORAGE_KEY);
13
16
  if (saved && $.maxTurnsSelect) {
14
17
  $.maxTurnsSelect.value = saved;
18
+ queueMicrotask(() => {
19
+ $.maxTurnsSelect?.dispatchEvent(new Event('change', { bubbles: true }));
20
+ });
15
21
  }
16
- $.maxTurnsSelect?.addEventListener('change', () => {
17
- localStorage.setItem(STORAGE_KEY, $.maxTurnsSelect.value);
18
- });
19
22
  }
20
23
 
21
24
  init();
@@ -443,3 +443,45 @@ function addForkButton(msgEl, messageId) {
443
443
  btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>`;
444
444
  msgEl.appendChild(btn);
445
445
  }
446
+
447
+ // ── Lazy-loading helpers ────────────────────────────────
448
+
449
+ export function prependOlderMessages(messages, pane) {
450
+ if (!messages || messages.length === 0) return;
451
+
452
+ // Render older messages into a detached container using the same rendering logic
453
+ const tempContainer = document.createElement("div");
454
+ const tempPane = { messagesDiv: tempContainer, currentAssistantMsg: null };
455
+ renderMessagesIntoPane(messages, tempPane);
456
+
457
+ // Capture scroll position before DOM mutation
458
+ const scrollHeightBefore = pane.messagesDiv.scrollHeight;
459
+
460
+ // Move all rendered nodes into the real pane
461
+ const fragment = document.createDocumentFragment();
462
+ while (tempContainer.firstChild) {
463
+ fragment.appendChild(tempContainer.firstChild);
464
+ }
465
+
466
+ // Insert after loading indicator (if present) or at the top
467
+ const indicator = pane.messagesDiv.querySelector(".load-more-indicator");
468
+ const insertRef = indicator ? indicator.nextSibling : pane.messagesDiv.firstChild;
469
+ pane.messagesDiv.insertBefore(fragment, insertRef);
470
+
471
+ // Restore scroll position so the user's view doesn't jump
472
+ const scrollHeightAfter = pane.messagesDiv.scrollHeight;
473
+ pane.messagesDiv.scrollTop += (scrollHeightAfter - scrollHeightBefore);
474
+ }
475
+
476
+ export function showLoadingIndicator(pane) {
477
+ if (pane.messagesDiv.querySelector(".load-more-indicator")) return;
478
+ const el = document.createElement("div");
479
+ el.className = "load-more-indicator";
480
+ el.innerHTML = '<span class="load-more-spinner"></span> Loading older messages\u2026';
481
+ pane.messagesDiv.prepend(el);
482
+ }
483
+
484
+ export function hideLoadingIndicator(pane) {
485
+ const el = pane.messagesDiv.querySelector(".load-more-indicator");
486
+ if (el) el.remove();
487
+ }
@@ -11,6 +11,7 @@ function init() {
11
11
  const saved = localStorage.getItem(STORAGE_KEY);
12
12
  if (saved && $.modelSelect) {
13
13
  $.modelSelect.value = saved;
14
+ $.modelSelect.dispatchEvent(new Event('change', { bubbles: true }));
14
15
  }
15
16
  $.modelSelect?.addEventListener('change', () => {
16
17
  localStorage.setItem(STORAGE_KEY, $.modelSelect.value);
@@ -138,11 +138,9 @@ export function enterParallelMode() {
138
138
 
139
139
  const sessionId = getState("sessionId");
140
140
  if (sessionId) {
141
- // Lazy import to avoid circular dependency
141
+ // Lazy import to avoid circular dependency — load all panes concurrently
142
142
  import('../features/sessions.js').then(({ loadPaneMessages }) => {
143
- for (const chatId of CHAT_IDS) {
144
- loadPaneMessages(sessionId, chatId);
145
- }
143
+ Promise.all(CHAT_IDS.map(chatId => loadPaneMessages(sessionId, chatId)));
146
144
  });
147
145
  }
148
146
  }
@@ -153,6 +153,7 @@ function initPermissions() {
153
153
  const saved = localStorage.getItem(STORAGE_KEY);
154
154
  if (saved && $.permModeSelect) {
155
155
  $.permModeSelect.value = saved;
156
+ $.permModeSelect.dispatchEvent(new Event('change', { bubbles: true }));
156
157
  }
157
158
 
158
159
  // Persist mode changes