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
@@ -0,0 +1,423 @@
1
+ // Message Queue — queue messages during streaming, auto-fire sequentially
2
+ import { $ } from '../core/dom.js';
3
+ import { getState } from '../core/store.js';
4
+ import { getPane } from '../ui/parallel.js';
5
+
6
+ let _queueIdCounter = 0;
7
+
8
+ // ── Lazy import to break circular dependency with chat.js ──
9
+ let _chatFns = null;
10
+ async function getChatFns() {
11
+ if (!_chatFns) {
12
+ _chatFns = await import('./chat.js');
13
+ }
14
+ return _chatFns;
15
+ }
16
+
17
+ // ── Pane initialization ──
18
+
19
+ export function initQueueOnPane(pane) {
20
+ pane._messageQueue = [];
21
+ pane._queuePaused = false;
22
+ pane._queuePauseReason = null;
23
+ }
24
+
25
+ // ── Core queue operations ──
26
+
27
+ export function enqueueMessage(text, pane) {
28
+ pane = pane || getPane(null);
29
+ pane._messageQueue.push({ id: ++_queueIdCounter, text });
30
+ renderQueueChips(pane);
31
+ }
32
+
33
+ export async function fireNextQueued(pane) {
34
+ pane = pane || getPane(null);
35
+ if (pane._messageQueue.length === 0 || pane._queuePaused) return;
36
+ const item = pane._messageQueue.shift();
37
+ renderQueueChips(pane);
38
+ const { _doSend } = await getChatFns();
39
+ _doSend(item.text, pane);
40
+ }
41
+
42
+ export function removeFromQueue(pane, index) {
43
+ pane = pane || getPane(null);
44
+ if (index >= 0 && index < pane._messageQueue.length) {
45
+ pane._messageQueue.splice(index, 1);
46
+ renderQueueChips(pane);
47
+ }
48
+ }
49
+
50
+ export function editQueueItem(pane, index, newText) {
51
+ pane = pane || getPane(null);
52
+ const trimmed = newText.trim();
53
+ if (!trimmed) {
54
+ removeFromQueue(pane, index);
55
+ return;
56
+ }
57
+ if (index >= 0 && index < pane._messageQueue.length) {
58
+ pane._messageQueue[index].text = trimmed;
59
+ renderQueueChips(pane);
60
+ }
61
+ }
62
+
63
+ export function reorderQueue(pane, fromIndex, toIndex) {
64
+ pane = pane || getPane(null);
65
+ if (fromIndex === toIndex) return;
66
+ const [item] = pane._messageQueue.splice(fromIndex, 1);
67
+ pane._messageQueue.splice(toIndex, 0, item);
68
+ renderQueueChips(pane);
69
+ }
70
+
71
+ export function clearQueue(pane) {
72
+ pane = pane || getPane(null);
73
+ pane._messageQueue = [];
74
+ pane._queuePaused = false;
75
+ pane._queuePauseReason = null;
76
+ renderQueueChips(pane);
77
+ }
78
+
79
+ export function getQueue(pane) {
80
+ pane = pane || getPane(null);
81
+ return [...pane._messageQueue];
82
+ }
83
+
84
+ // ── Pause / Resume ──
85
+
86
+ export function pauseQueue(pane, reason) {
87
+ pane = pane || getPane(null);
88
+ pane._queuePaused = true;
89
+ pane._queuePauseReason = reason || 'user';
90
+ renderQueueChips(pane);
91
+ }
92
+
93
+ export function resumeQueue(pane) {
94
+ pane = pane || getPane(null);
95
+ pane._queuePaused = false;
96
+ pane._queuePauseReason = null;
97
+ renderQueueChips(pane);
98
+ }
99
+
100
+ // ── Stop with queue (3-option modal) ──
101
+
102
+ export function handleStopWithQueue(pane) {
103
+ pane = pane || getPane(null);
104
+ const modal = $.queueStopModal;
105
+ if (!modal) return;
106
+
107
+ // Populate preview
108
+ const preview = $.queueStopPreview;
109
+ if (preview) {
110
+ preview.innerHTML = '';
111
+ for (const item of pane._messageQueue) {
112
+ const el = document.createElement('span');
113
+ el.className = 'mq-queue-preview-item';
114
+ el.textContent = truncateText(item.text, 40);
115
+ preview.appendChild(el);
116
+ }
117
+ }
118
+
119
+ modal.classList.remove('hidden');
120
+
121
+ const cleanup = () => {
122
+ modal.classList.add('hidden');
123
+ $.queueStopAll.removeEventListener('click', onTerminate);
124
+ $.queueStopSkip.removeEventListener('click', onSkip);
125
+ $.queueStopPause.removeEventListener('click', onPause);
126
+ modal.removeEventListener('click', onBackdrop);
127
+ };
128
+
129
+ const sendAbort = () => {
130
+ const ws = getState("ws");
131
+ if (ws && ws.readyState === WebSocket.OPEN) {
132
+ const payload = { type: "abort" };
133
+ const parallelMode = getState("parallelMode");
134
+ if (parallelMode && pane.chatId) {
135
+ payload.chatId = pane.chatId;
136
+ }
137
+ ws.send(JSON.stringify(payload));
138
+ }
139
+ };
140
+
141
+ const onTerminate = () => {
142
+ clearQueue(pane);
143
+ sendAbort();
144
+ cleanup();
145
+ };
146
+
147
+ const onSkip = () => {
148
+ // Don't pause — finishStreamingHandler will auto-fire next
149
+ sendAbort();
150
+ cleanup();
151
+ };
152
+
153
+ const onPause = () => {
154
+ pauseQueue(pane, 'user');
155
+ sendAbort();
156
+ cleanup();
157
+ };
158
+
159
+ const onBackdrop = (e) => {
160
+ if (e.target === modal) cleanup();
161
+ };
162
+
163
+ $.queueStopAll.addEventListener('click', onTerminate);
164
+ $.queueStopSkip.addEventListener('click', onSkip);
165
+ $.queueStopPause.addEventListener('click', onPause);
166
+ modal.addEventListener('click', onBackdrop);
167
+ }
168
+
169
+ // ── Chip rendering ──
170
+
171
+ function truncateText(text, max) {
172
+ if (text.length <= max) return text;
173
+ return text.slice(0, max) + '...';
174
+ }
175
+
176
+ function getChipContainer(pane) {
177
+ const parallelMode = getState("parallelMode");
178
+ if (parallelMode) {
179
+ // In parallel mode, the textarea is a direct child of the input-bar
180
+ const inputBar = pane.messageInput?.closest('.input-bar');
181
+ return inputBar || null;
182
+ }
183
+ // Single mode: use .input-textarea-wrap
184
+ const wrap = $.messageInput?.closest('.input-textarea-wrap');
185
+ return wrap || null;
186
+ }
187
+
188
+ export function renderQueueChips(pane) {
189
+ pane = pane || getPane(null);
190
+ const container = getChipContainer(pane);
191
+ if (!container) return;
192
+
193
+ // Find or create strip
194
+ let strip = container.querySelector('.mq-chip-strip');
195
+ if (pane._messageQueue.length === 0) {
196
+ if (strip) strip.remove();
197
+ return;
198
+ }
199
+
200
+ if (!strip) {
201
+ strip = document.createElement('div');
202
+ strip.className = 'mq-chip-strip';
203
+ // Insert as first child (above textarea)
204
+ container.insertBefore(strip, container.firstChild);
205
+ }
206
+
207
+ strip.innerHTML = '';
208
+
209
+ // Drag state
210
+ let dragItem = null;
211
+ let dragFromIndex = -1;
212
+ let dragPlaceholder = null;
213
+
214
+ pane._messageQueue.forEach((item, index) => {
215
+ const chip = document.createElement('div');
216
+ chip.className = 'mq-chip';
217
+ chip.draggable = true;
218
+ chip.dataset.queueIndex = index;
219
+
220
+ const num = document.createElement('span');
221
+ num.className = 'mq-chip-num';
222
+ num.textContent = index + 1;
223
+
224
+ const handle = document.createElement('span');
225
+ handle.className = 'mq-chip-handle';
226
+ handle.textContent = '\u2807'; // ⠇
227
+
228
+ const text = document.createElement('span');
229
+ text.className = 'mq-chip-text';
230
+ text.textContent = truncateText(item.text, 40);
231
+ text.title = item.text;
232
+
233
+ const editBtn = document.createElement('button');
234
+ editBtn.className = 'mq-chip-edit';
235
+ editBtn.title = 'Edit';
236
+ editBtn.innerHTML = '&#9998;'; // ✎
237
+ editBtn.addEventListener('click', (e) => {
238
+ e.stopPropagation();
239
+ showInlineEditor(chip, item, pane, index);
240
+ });
241
+
242
+ const removeBtn = document.createElement('button');
243
+ removeBtn.className = 'mq-chip-remove';
244
+ removeBtn.title = 'Remove';
245
+ removeBtn.textContent = '\u00d7'; // ×
246
+ removeBtn.addEventListener('click', (e) => {
247
+ e.stopPropagation();
248
+ removeFromQueue(pane, index);
249
+ });
250
+
251
+ chip.appendChild(num);
252
+ chip.appendChild(text);
253
+ chip.appendChild(handle);
254
+ chip.appendChild(editBtn);
255
+ chip.appendChild(removeBtn);
256
+
257
+ // Drag events
258
+ chip.addEventListener('dragstart', (e) => {
259
+ dragItem = chip;
260
+ dragFromIndex = index;
261
+ chip.classList.add('dragging');
262
+ e.dataTransfer.effectAllowed = 'move';
263
+ dragPlaceholder = document.createElement('div');
264
+ dragPlaceholder.className = 'mq-drop-indicator';
265
+ requestAnimationFrame(() => { chip.style.opacity = '0.4'; });
266
+ });
267
+
268
+ chip.addEventListener('dragend', () => {
269
+ if (dragItem) {
270
+ dragItem.classList.remove('dragging');
271
+ dragItem.style.opacity = '';
272
+ }
273
+ if (dragPlaceholder?.parentNode) dragPlaceholder.remove();
274
+ dragItem = null;
275
+ dragFromIndex = -1;
276
+ dragPlaceholder = null;
277
+ });
278
+
279
+ chip.addEventListener('dragover', (e) => {
280
+ e.preventDefault();
281
+ e.dataTransfer.dropEffect = 'move';
282
+ if (!dragItem || dragItem === chip) return;
283
+ const rect = chip.getBoundingClientRect();
284
+ const after = e.clientX > rect.left + rect.width / 2;
285
+ if (after) chip.after(dragPlaceholder);
286
+ else chip.before(dragPlaceholder);
287
+ });
288
+
289
+ chip.addEventListener('drop', (e) => {
290
+ e.preventDefault();
291
+ if (!dragItem || dragItem === chip) return;
292
+ // Calculate target index from placeholder position
293
+ const chips = [...strip.querySelectorAll('.mq-chip')];
294
+ const placeholderIdx = [...strip.children].indexOf(dragPlaceholder);
295
+ let toIndex = 0;
296
+ let chipsBefore = 0;
297
+ for (const child of strip.children) {
298
+ if (child === dragPlaceholder) break;
299
+ if (child.classList.contains('mq-chip')) chipsBefore++;
300
+ }
301
+ toIndex = chipsBefore;
302
+ if (dragFromIndex < toIndex) toIndex--;
303
+
304
+ if (dragPlaceholder?.parentNode) dragPlaceholder.remove();
305
+ dragItem.classList.remove('dragging');
306
+ dragItem.style.opacity = '';
307
+ reorderQueue(pane, dragFromIndex, toIndex);
308
+ dragItem = null;
309
+ dragFromIndex = -1;
310
+ dragPlaceholder = null;
311
+ });
312
+
313
+ strip.appendChild(chip);
314
+ });
315
+
316
+ // Queue count
317
+ const count = document.createElement('span');
318
+ count.className = 'mq-chip-count';
319
+ count.textContent = `${pane._messageQueue.length} queued`;
320
+ strip.appendChild(count);
321
+
322
+ // Paused indicator
323
+ if (pane._queuePaused) {
324
+ const paused = document.createElement('span');
325
+ paused.className = 'mq-queue-paused';
326
+ const reasonLabel = pane._queuePauseReason === 'error' ? 'error'
327
+ : pane._queuePauseReason === 'question' ? 'waiting for response'
328
+ : 'stopped';
329
+ paused.innerHTML = `\u23F8 Paused (${reasonLabel}) `;
330
+ const resumeBtn = document.createElement('button');
331
+ resumeBtn.className = 'mq-resume-btn';
332
+ resumeBtn.textContent = 'Resume';
333
+ resumeBtn.addEventListener('click', () => {
334
+ resumeQueue(pane);
335
+ // Auto-fire if not streaming
336
+ if (!pane.isStreaming && pane._messageQueue.length > 0) {
337
+ fireNextQueued(pane);
338
+ }
339
+ });
340
+ paused.appendChild(resumeBtn);
341
+ strip.appendChild(paused);
342
+ }
343
+ }
344
+
345
+ // ── Inline editor popover ──
346
+
347
+ function showInlineEditor(chipEl, item, pane, index) {
348
+ // Close any existing editor
349
+ closeAllEditors();
350
+
351
+ const popover = document.createElement('div');
352
+ popover.className = 'mq-editor-popover';
353
+
354
+ const textarea = document.createElement('textarea');
355
+ textarea.className = 'mq-editor-textarea';
356
+ textarea.rows = 3;
357
+ textarea.value = item.text;
358
+
359
+ const actions = document.createElement('div');
360
+ actions.className = 'mq-editor-actions';
361
+
362
+ const hint = document.createElement('span');
363
+ hint.className = 'mq-editor-hint';
364
+ hint.textContent = 'Ctrl+Enter to save';
365
+
366
+ const buttons = document.createElement('div');
367
+ buttons.className = 'mq-editor-buttons';
368
+
369
+ const saveBtn = document.createElement('button');
370
+ saveBtn.className = 'mq-editor-save';
371
+ saveBtn.textContent = 'Save';
372
+
373
+ const cancelBtn = document.createElement('button');
374
+ cancelBtn.className = 'mq-editor-cancel';
375
+ cancelBtn.textContent = 'Cancel';
376
+
377
+ const close = () => popover.remove();
378
+
379
+ saveBtn.addEventListener('click', () => {
380
+ editQueueItem(pane, index, textarea.value);
381
+ close();
382
+ });
383
+
384
+ cancelBtn.addEventListener('click', close);
385
+
386
+ textarea.addEventListener('keydown', (e) => {
387
+ if (e.key === 'Escape') {
388
+ e.stopPropagation();
389
+ close();
390
+ }
391
+ if (e.key === 'Enter' && e.ctrlKey) {
392
+ e.preventDefault();
393
+ editQueueItem(pane, index, textarea.value);
394
+ close();
395
+ }
396
+ });
397
+
398
+ buttons.appendChild(cancelBtn);
399
+ buttons.appendChild(saveBtn);
400
+ actions.appendChild(hint);
401
+ actions.appendChild(buttons);
402
+ popover.appendChild(textarea);
403
+ popover.appendChild(actions);
404
+ chipEl.appendChild(popover);
405
+
406
+ // Focus textarea
407
+ requestAnimationFrame(() => textarea.focus());
408
+
409
+ // Close on outside click (deferred so this click doesn't trigger it)
410
+ requestAnimationFrame(() => {
411
+ const onOutside = (e) => {
412
+ if (!popover.contains(e.target) && !chipEl.contains(e.target)) {
413
+ close();
414
+ document.removeEventListener('mousedown', onOutside);
415
+ }
416
+ };
417
+ document.addEventListener('mousedown', onOutside);
418
+ });
419
+ }
420
+
421
+ function closeAllEditors() {
422
+ document.querySelectorAll('.mq-editor-popover').forEach(el => el.remove());
423
+ }
@@ -151,9 +151,81 @@ export async function loadProjectCommands() {
151
151
  // ── Add Project (folder browser) ────────────────────────
152
152
  let currentBrowsePath = "";
153
153
 
154
+ const RECENTS_KEY = "claudeck-folder-recents";
155
+ const RECENTS_MAX = 5;
156
+
157
+ function loadRecents() {
158
+ try {
159
+ const raw = localStorage.getItem(RECENTS_KEY);
160
+ const arr = raw ? JSON.parse(raw) : [];
161
+ return Array.isArray(arr) ? arr.filter((p) => typeof p === "string") : [];
162
+ } catch {
163
+ return [];
164
+ }
165
+ }
166
+
167
+ function pushRecent(path) {
168
+ if (!path) return;
169
+ const existing = loadRecents().filter((p) => p !== path);
170
+ existing.unshift(path);
171
+ localStorage.setItem(RECENTS_KEY, JSON.stringify(existing.slice(0, RECENTS_MAX)));
172
+ }
173
+
174
+ function renderRecents() {
175
+ const recents = loadRecents();
176
+ $.folderRecents.innerHTML = "";
177
+ if (recents.length === 0) {
178
+ $.folderRecents.classList.add("hidden");
179
+ return;
180
+ }
181
+ $.folderRecents.classList.remove("hidden");
182
+ const label = document.createElement("span");
183
+ label.className = "folder-recents-label";
184
+ label.textContent = "Recent:";
185
+ $.folderRecents.appendChild(label);
186
+ for (const p of recents) {
187
+ const chip = document.createElement("button");
188
+ chip.type = "button";
189
+ chip.className = "folder-recent-chip";
190
+ chip.title = p;
191
+ chip.textContent = p.split(/[/\\]/).filter(Boolean).pop() || p;
192
+ chip.addEventListener("click", () => navigateToDir(p));
193
+ $.folderRecents.appendChild(chip);
194
+ }
195
+ }
196
+
197
+ function showMessage(text, kind = "error", action) {
198
+ if (!text) {
199
+ hideMessage();
200
+ return;
201
+ }
202
+ $.addProjectMessage.innerHTML = "";
203
+ $.addProjectMessage.className = `add-project-message add-project-message-${kind}`;
204
+ const span = document.createElement("span");
205
+ span.textContent = text;
206
+ $.addProjectMessage.appendChild(span);
207
+ if (action) {
208
+ const btn = document.createElement("button");
209
+ btn.type = "button";
210
+ btn.className = "add-project-message-action";
211
+ btn.textContent = action.label;
212
+ btn.addEventListener("click", action.onClick);
213
+ $.addProjectMessage.appendChild(btn);
214
+ }
215
+ }
216
+
217
+ function hideMessage() {
218
+ $.addProjectMessage.className = "add-project-message hidden";
219
+ $.addProjectMessage.innerHTML = "";
220
+ }
221
+
154
222
  function openAddProjectModal() {
155
223
  $.addProjectModal.classList.remove("hidden");
156
224
  $.addProjectName.value = "";
225
+ $.folderPathInput.value = "";
226
+ hideMessage();
227
+ hideNewFolderRow();
228
+ renderRecents();
157
229
  navigateToDir(""); // defaults to $HOME on server
158
230
  }
159
231
 
@@ -163,6 +235,8 @@ function closeAddProjectModal() {
163
235
 
164
236
  async function navigateToDir(dir) {
165
237
  $.folderList.innerHTML = '<div class="folder-list-loading">Loading...</div>';
238
+ hideMessage();
239
+ hideNewFolderRow();
166
240
  try {
167
241
  const data = await api.browseFolders(dir || undefined);
168
242
  currentBrowsePath = data.current;
@@ -171,11 +245,84 @@ async function navigateToDir(dir) {
171
245
  // Auto-fill name from last segment
172
246
  const base = data.current.split(/[/\\]/).filter(Boolean).pop() || "";
173
247
  $.addProjectName.value = base;
248
+ $.folderPathInput.value = data.current;
174
249
  } catch (err) {
175
250
  $.folderList.innerHTML = `<div class="folder-list-empty">Error: ${err.message}</div>`;
176
251
  }
177
252
  }
178
253
 
254
+ function showNewFolderRow() {
255
+ $.folderNewRow.classList.remove("hidden");
256
+ $.folderNewToggleRow.classList.add("hidden");
257
+ $.folderNewName.value = "";
258
+ $.folderNewName.focus();
259
+ }
260
+
261
+ function hideNewFolderRow() {
262
+ $.folderNewRow.classList.add("hidden");
263
+ $.folderNewToggleRow.classList.remove("hidden");
264
+ }
265
+
266
+ async function createFolderInline() {
267
+ const name = $.folderNewName.value.trim();
268
+ if (!name) {
269
+ $.folderNewName.focus();
270
+ return;
271
+ }
272
+ if (/[\/\\]/.test(name) || name === "." || name === "..") {
273
+ showMessage("Folder name cannot contain '/', '\\', or be '.' / '..'", "error");
274
+ return;
275
+ }
276
+ if (!currentBrowsePath) return;
277
+
278
+ try {
279
+ const result = await api.createFolder(currentBrowsePath, name);
280
+ hideNewFolderRow();
281
+ await navigateToDir(result.path);
282
+ showMessage(`Created "${name}". Click Add to use it as a project.`, "success");
283
+ } catch (err) {
284
+ if (err.status === 409) {
285
+ showMessage(`"${name}" already exists here.`, "error", {
286
+ label: "Open it",
287
+ onClick: async () => {
288
+ const target = currentBrowsePath.replace(/\/$/, "") + "/" + name;
289
+ hideNewFolderRow();
290
+ await navigateToDir(target);
291
+ },
292
+ });
293
+ } else {
294
+ showMessage(err.message || "Failed to create folder", "error");
295
+ }
296
+ }
297
+ }
298
+
299
+ async function goToTypedPath() {
300
+ const raw = $.folderPathInput.value.trim();
301
+ if (!raw) return;
302
+ // Expand leading ~ to $HOME on the server side — client just passes it along;
303
+ // the server resolves. But browse route uses resolve() which does not expand ~.
304
+ // So expand here against a best-effort guess: leave ~ as-is and let the user
305
+ // type absolute paths, OR expand client-side using a hardcoded "~" marker.
306
+ // Simpler: if the user types "~", ask the server to browse default (homedir)
307
+ // and then append the rest.
308
+ let target = raw;
309
+ if (raw === "~") {
310
+ await navigateToDir(""); // defaults to $HOME on server
311
+ return;
312
+ }
313
+ if (raw.startsWith("~/")) {
314
+ // Browse home first to learn its absolute path, then descend.
315
+ try {
316
+ const home = await api.browseFolders(undefined);
317
+ target = home.current.replace(/\/$/, "") + "/" + raw.slice(2);
318
+ } catch {
319
+ showMessage("Could not resolve ~", "error");
320
+ return;
321
+ }
322
+ }
323
+ await navigateToDir(target);
324
+ }
325
+
179
326
  function renderBreadcrumb(pathStr) {
180
327
  $.folderBreadcrumb.innerHTML = "";
181
328
  const parts = pathStr.split(/[/\\]/).filter(Boolean);
@@ -237,18 +384,30 @@ function renderFolderList(data) {
237
384
  }
238
385
  }
239
386
 
387
+ function selectExistingProject(path) {
388
+ if (![...$.projectSelect.options].some((o) => o.value === path)) return false;
389
+ $.projectSelect.value = path;
390
+ $.projectSelect.dispatchEvent(new Event("change"));
391
+ closeAddProjectModal();
392
+ return true;
393
+ }
394
+
240
395
  async function confirmAddProject() {
241
396
  const name = $.addProjectName.value.trim();
242
397
  if (!name) {
398
+ showMessage("Enter a project name.", "error");
243
399
  $.addProjectName.focus();
244
400
  return;
245
401
  }
246
402
  if (!currentBrowsePath) return;
247
403
 
248
- // Check for duplicate in dropdown
404
+ // Check for duplicate in dropdown — offer to switch instead of blocking.
249
405
  const existing = [...$.projectSelect.options].find((o) => o.value === currentBrowsePath);
250
406
  if (existing) {
251
- alert("This project path is already added.");
407
+ showMessage("This folder is already added as a project.", "error", {
408
+ label: "Switch to it",
409
+ onClick: () => selectExistingProject(currentBrowsePath),
410
+ });
252
411
  return;
253
412
  }
254
413
 
@@ -268,6 +427,7 @@ async function confirmAddProject() {
268
427
  projects.push({ name: project.name, path: project.path });
269
428
 
270
429
  localStorage.setItem("claudeck-cwd", project.path);
430
+ pushRecent(project.path);
271
431
  updateSystemPromptIndicator();
272
432
  updateHeaderProjectName();
273
433
  updateSessionControls();
@@ -277,7 +437,7 @@ async function confirmAddProject() {
277
437
 
278
438
  closeAddProjectModal();
279
439
  } catch (err) {
280
- alert("Failed to add project: " + err.message);
440
+ showMessage("Failed to add project: " + err.message, "error");
281
441
  }
282
442
  }
283
443
 
@@ -332,6 +492,28 @@ $.addProjectModal.addEventListener("click", (e) => {
332
492
  if (e.target === $.addProjectModal) closeAddProjectModal();
333
493
  });
334
494
 
495
+ // Typed path navigation
496
+ $.folderPathGo.addEventListener("click", goToTypedPath);
497
+ $.folderPathInput.addEventListener("keydown", (e) => {
498
+ if (e.key === "Enter") {
499
+ e.preventDefault();
500
+ goToTypedPath();
501
+ }
502
+ });
503
+
504
+ // New folder inline row
505
+ $.folderNewToggle.addEventListener("click", showNewFolderRow);
506
+ $.folderNewCancel.addEventListener("click", hideNewFolderRow);
507
+ $.folderNewCreate.addEventListener("click", createFolderInline);
508
+ $.folderNewName.addEventListener("keydown", (e) => {
509
+ if (e.key === "Enter") {
510
+ e.preventDefault();
511
+ createFolderInline();
512
+ } else if (e.key === "Escape") {
513
+ hideNewFolderRow();
514
+ }
515
+ });
516
+
335
517
  // System prompt modal event listeners
336
518
  $.spEditBtn.addEventListener("click", openSystemPromptModal);
337
519
  $.spForm.addEventListener("submit", async (e) => {
package/public/js/main.js CHANGED
@@ -14,11 +14,13 @@ import './components/file-picker-modal.js';
14
14
  import './components/shortcuts-modal.js';
15
15
  import './components/cost-dashboard-modal.js';
16
16
  import './components/bg-confirm-modal.js';
17
+ import './components/queue-stop-modal.js';
17
18
  import './components/permission-modal.js';
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';
23
+ import './components/jump-to-latest.js';
22
24
  import './components/status-bar.js';
23
25
 
24
26
  import './core/store.js';
@@ -63,6 +65,7 @@ import './features/easter-egg.js';
63
65
  import './features/welcome.js';
64
66
  import './features/home.js';
65
67
  import './features/chat.js';
68
+ import './features/message-queue.js';
66
69
  import './panels/file-explorer.js';
67
70
  import './panels/git-panel.js';
68
71
  import './panels/mcp-manager.js';