claudeck 1.4.1 → 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.
- package/README.md +2 -4
- package/package.json +1 -1
- package/public/css/core/theme.css +6 -21
- package/public/css/core/variables.css +2 -0
- package/public/css/features/message-queue.css +348 -0
- package/public/css/ui/commands.css +4 -4
- package/public/css/ui/messages.css +310 -78
- package/public/css/ui/sessions.css +173 -0
- package/public/index.html +3 -2
- package/public/js/components/add-project-modal.js +14 -0
- package/public/js/components/jump-to-latest.js +42 -0
- package/public/js/components/queue-stop-modal.js +23 -0
- package/public/js/core/api.js +15 -43
- package/public/js/core/dom.js +17 -0
- package/public/js/core/utils.js +38 -2
- package/public/js/features/chat.js +49 -1
- package/public/js/features/message-queue.js +423 -0
- package/public/js/features/projects.js +185 -3
- package/public/js/main.js +3 -1
- package/public/js/panels/dev-docs.js +1 -1
- package/public/js/ui/formatting.js +65 -11
- package/public/js/ui/messages.js +97 -1
- package/public/js/ui/parallel.js +32 -2
- package/public/js/ui/right-panel.js +0 -8
- package/public/style.css +1 -0
- package/server/routes/projects.js +0 -0
- package/plugins/linear/client.css +0 -345
- package/plugins/linear/client.js +0 -380
- package/plugins/linear/config.json +0 -5
- package/plugins/linear/manifest.json +0 -10
- package/plugins/linear/server.js +0 -312
- 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 = '✎'; // ✎
|
|
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
|
-
|
|
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
|
-
|
|
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,12 +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
21
|
import './components/settings-modal.js';
|
|
22
22
|
import './components/add-project-modal.js';
|
|
23
|
+
import './components/jump-to-latest.js';
|
|
23
24
|
import './components/status-bar.js';
|
|
24
25
|
|
|
25
26
|
import './core/store.js';
|
|
@@ -64,6 +65,7 @@ import './features/easter-egg.js';
|
|
|
64
65
|
import './features/welcome.js';
|
|
65
66
|
import './features/home.js';
|
|
66
67
|
import './features/chat.js';
|
|
68
|
+
import './features/message-queue.js';
|
|
67
69
|
import './panels/file-explorer.js';
|
|
68
70
|
import './panels/git-panel.js';
|
|
69
71
|
import './panels/mcp-manager.js';
|
|
@@ -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/claude-editor/</code> or <code>plugins/repos/</code> as reference implementations. For full-stack with server routes, see
|
|
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 plugins with server routes, see the <a href="https://github.com/hamedafarag/claudeck-marketplace" target="_blank">marketplace repo</a> (e.g. the Linear plugin).</div>
|
|
305
305
|
`,
|
|
306
306
|
});
|
|
307
307
|
|