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
|
@@ -287,6 +287,179 @@
|
|
|
287
287
|
font-family: var(--font-mono);
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
/* Typed path row */
|
|
291
|
+
.folder-path-row {
|
|
292
|
+
display: flex;
|
|
293
|
+
gap: 6px;
|
|
294
|
+
align-items: center;
|
|
295
|
+
margin-bottom: 8px;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.folder-path-row input {
|
|
299
|
+
flex: 1;
|
|
300
|
+
padding: 8px 10px;
|
|
301
|
+
font-size: 12px;
|
|
302
|
+
font-family: var(--font-mono);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.folder-path-go {
|
|
306
|
+
flex-shrink: 0;
|
|
307
|
+
padding: 7px 14px;
|
|
308
|
+
font-size: 11px;
|
|
309
|
+
font-family: var(--font-mono);
|
|
310
|
+
background: var(--bg);
|
|
311
|
+
color: var(--text-secondary);
|
|
312
|
+
border: 1px solid var(--border);
|
|
313
|
+
border-radius: var(--radius-md);
|
|
314
|
+
cursor: pointer;
|
|
315
|
+
transition: all 0.15s;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.folder-path-go:hover {
|
|
319
|
+
color: var(--accent);
|
|
320
|
+
border-color: var(--accent);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/* Recents row */
|
|
324
|
+
.folder-recents {
|
|
325
|
+
display: flex;
|
|
326
|
+
align-items: center;
|
|
327
|
+
flex-wrap: wrap;
|
|
328
|
+
gap: 6px;
|
|
329
|
+
padding: 6px 0 10px;
|
|
330
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
331
|
+
margin-bottom: 6px;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.folder-recents-label {
|
|
335
|
+
font-size: 10px;
|
|
336
|
+
text-transform: uppercase;
|
|
337
|
+
letter-spacing: 0.5px;
|
|
338
|
+
color: var(--text-dim);
|
|
339
|
+
margin-right: 2px;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.folder-recent-chip {
|
|
343
|
+
padding: 3px 8px;
|
|
344
|
+
font-size: 11px;
|
|
345
|
+
font-family: var(--font-mono);
|
|
346
|
+
color: var(--text-secondary);
|
|
347
|
+
background: var(--bg);
|
|
348
|
+
border: 1px solid var(--border);
|
|
349
|
+
border-radius: 999px;
|
|
350
|
+
cursor: pointer;
|
|
351
|
+
transition: all 0.15s;
|
|
352
|
+
max-width: 160px;
|
|
353
|
+
overflow: hidden;
|
|
354
|
+
text-overflow: ellipsis;
|
|
355
|
+
white-space: nowrap;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.folder-recent-chip:hover {
|
|
359
|
+
color: var(--accent);
|
|
360
|
+
border-color: var(--accent);
|
|
361
|
+
background: var(--accent-dim);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/* New folder row */
|
|
365
|
+
.folder-new-toggle-row {
|
|
366
|
+
margin: 4px 0 8px;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.folder-new-toggle {
|
|
370
|
+
background: none;
|
|
371
|
+
border: 1px dashed var(--border);
|
|
372
|
+
border-radius: var(--radius-md);
|
|
373
|
+
color: var(--text-dim);
|
|
374
|
+
font-family: var(--font-mono);
|
|
375
|
+
font-size: 11px;
|
|
376
|
+
padding: 6px 10px;
|
|
377
|
+
width: 100%;
|
|
378
|
+
cursor: pointer;
|
|
379
|
+
transition: all 0.15s;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.folder-new-toggle:hover {
|
|
383
|
+
color: var(--accent);
|
|
384
|
+
border-color: var(--accent);
|
|
385
|
+
background: var(--accent-dim);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.folder-new-row {
|
|
389
|
+
display: flex;
|
|
390
|
+
gap: 6px;
|
|
391
|
+
align-items: center;
|
|
392
|
+
margin: 4px 0 8px;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.folder-new-row input {
|
|
396
|
+
flex: 1;
|
|
397
|
+
padding: 8px 10px;
|
|
398
|
+
font-size: 12px;
|
|
399
|
+
font-family: var(--font-mono);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.modal-btn-cancel {
|
|
403
|
+
flex-shrink: 0;
|
|
404
|
+
padding: 7px 12px;
|
|
405
|
+
font-size: 11px;
|
|
406
|
+
font-family: var(--font-mono);
|
|
407
|
+
background: transparent;
|
|
408
|
+
color: var(--text-dim);
|
|
409
|
+
border: 1px solid var(--border);
|
|
410
|
+
border-radius: var(--radius-md);
|
|
411
|
+
cursor: pointer;
|
|
412
|
+
transition: all 0.15s;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.modal-btn-cancel:hover {
|
|
416
|
+
color: var(--text);
|
|
417
|
+
border-color: var(--text-dim);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/* Inline message area (error / success) */
|
|
421
|
+
.add-project-message {
|
|
422
|
+
display: flex;
|
|
423
|
+
align-items: center;
|
|
424
|
+
justify-content: space-between;
|
|
425
|
+
gap: 8px;
|
|
426
|
+
padding: 8px 12px;
|
|
427
|
+
margin-bottom: 10px;
|
|
428
|
+
border-radius: var(--radius-md);
|
|
429
|
+
font-size: 12px;
|
|
430
|
+
font-family: var(--font-mono);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.add-project-message-error {
|
|
434
|
+
background: rgba(255, 80, 80, 0.08);
|
|
435
|
+
color: #ff8080;
|
|
436
|
+
border: 1px solid rgba(255, 80, 80, 0.3);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.add-project-message-success {
|
|
440
|
+
background: rgba(80, 200, 120, 0.08);
|
|
441
|
+
color: #6ad48a;
|
|
442
|
+
border: 1px solid rgba(80, 200, 120, 0.3);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.add-project-message-action {
|
|
446
|
+
flex-shrink: 0;
|
|
447
|
+
padding: 3px 10px;
|
|
448
|
+
font-size: 11px;
|
|
449
|
+
font-family: var(--font-mono);
|
|
450
|
+
background: transparent;
|
|
451
|
+
color: inherit;
|
|
452
|
+
border: 1px solid currentColor;
|
|
453
|
+
border-radius: var(--radius-md);
|
|
454
|
+
cursor: pointer;
|
|
455
|
+
opacity: 0.85;
|
|
456
|
+
transition: opacity 0.15s;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.add-project-message-action:hover {
|
|
460
|
+
opacity: 1;
|
|
461
|
+
}
|
|
462
|
+
|
|
290
463
|
/* Sessions */
|
|
291
464
|
.sessions-section {
|
|
292
465
|
flex: 1;
|
package/public/index.html
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<link rel="apple-touch-icon" href="/icons/icon-192.png">
|
|
13
13
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
14
14
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
15
|
-
<link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;500;600;700&family=Outfit:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
15
|
+
<link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;500;600;700&family=Source+Serif+4:ital,opsz,wght@0,8..60,400;0,8..60,500;0,8..60,600;0,8..60,700;1,8..60,400;1,8..60,500&family=Outfit:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
16
16
|
<link rel="stylesheet" href="style.css">
|
|
17
17
|
<link id="hljs-theme" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
|
18
18
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/driver.js@1.3.6/dist/driver.css">
|
|
@@ -324,6 +324,7 @@
|
|
|
324
324
|
<span class="input-waiting-dot"></span>
|
|
325
325
|
<span class="input-waiting-text">Claude is waiting for your response</span>
|
|
326
326
|
</div>
|
|
327
|
+
<claudeck-jump-to-latest></claudeck-jump-to-latest>
|
|
327
328
|
<div class="input-bar">
|
|
328
329
|
<div class="input-textarea-wrap">
|
|
329
330
|
<textarea id="message-input" placeholder="> type a message or / for commands..." rows="1"></textarea>
|
|
@@ -521,8 +522,8 @@
|
|
|
521
522
|
<claudeck-shortcuts-modal></claudeck-shortcuts-modal>
|
|
522
523
|
<claudeck-cost-dashboard></claudeck-cost-dashboard>
|
|
523
524
|
<claudeck-bg-confirm></claudeck-bg-confirm>
|
|
525
|
+
<claudeck-queue-stop></claudeck-queue-stop>
|
|
524
526
|
<claudeck-permission-modal></claudeck-permission-modal>
|
|
525
|
-
<claudeck-linear-create></claudeck-linear-create>
|
|
526
527
|
<claudeck-telegram-modal></claudeck-telegram-modal>
|
|
527
528
|
<claudeck-mcp-modal></claudeck-mcp-modal>
|
|
528
529
|
<claudeck-settings-modal></claudeck-settings-modal>
|
|
@@ -8,8 +8,22 @@ class AddProjectModal extends HTMLElement {
|
|
|
8
8
|
<button id="add-project-close" class="modal-close">×</button>
|
|
9
9
|
</div>
|
|
10
10
|
<div class="add-project-body">
|
|
11
|
+
<div class="folder-path-row">
|
|
12
|
+
<input id="folder-path-input" type="text" placeholder="Type a path (e.g. ~/code/my-app) and press Enter" autocomplete="off" spellcheck="false">
|
|
13
|
+
<button id="folder-path-go" class="folder-path-go" title="Go to path">Go</button>
|
|
14
|
+
</div>
|
|
15
|
+
<div id="folder-recents" class="folder-recents hidden"></div>
|
|
11
16
|
<div id="folder-breadcrumb" class="folder-breadcrumb"></div>
|
|
12
17
|
<div id="folder-list" class="folder-list"></div>
|
|
18
|
+
<div id="folder-new-row" class="folder-new-row hidden">
|
|
19
|
+
<input id="folder-new-name" type="text" placeholder="New folder name" autocomplete="off" spellcheck="false">
|
|
20
|
+
<button id="folder-new-create" class="modal-btn-save">Create</button>
|
|
21
|
+
<button id="folder-new-cancel" class="modal-btn-cancel">Cancel</button>
|
|
22
|
+
</div>
|
|
23
|
+
<div id="folder-new-toggle-row" class="folder-new-toggle-row">
|
|
24
|
+
<button id="folder-new-toggle" class="folder-new-toggle" type="button">+ New folder here</button>
|
|
25
|
+
</div>
|
|
26
|
+
<div id="add-project-message" class="add-project-message hidden"></div>
|
|
13
27
|
<div class="folder-select-row">
|
|
14
28
|
<input id="add-project-name" type="text" placeholder="Project name" autocomplete="off">
|
|
15
29
|
<button id="add-project-confirm" class="modal-btn-save">Add</button>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Jump-to-latest pill — shown when new content arrives while the user has
|
|
2
|
+
// scrolled up. Click to force-scroll to the bottom and re-engage stick-to-bottom.
|
|
3
|
+
class JumpToLatest extends HTMLElement {
|
|
4
|
+
connectedCallback() {
|
|
5
|
+
this.innerHTML = `
|
|
6
|
+
<button id="jump-to-latest-btn" class="jump-to-latest hidden" type="button" aria-label="Jump to latest message">
|
|
7
|
+
<svg width="14" height="14" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
|
8
|
+
<path d="M5 8l5 5 5-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
9
|
+
</svg>
|
|
10
|
+
<span class="jump-to-latest-label">New messages</span>
|
|
11
|
+
</button>`;
|
|
12
|
+
|
|
13
|
+
this._btn = this.querySelector('#jump-to-latest-btn');
|
|
14
|
+
this._onState = (e) => {
|
|
15
|
+
// Only react to the active (single-mode) pane for now. In parallel mode
|
|
16
|
+
// each pane handles its own scroll independently and the global pill stays hidden.
|
|
17
|
+
const detail = e.detail || {};
|
|
18
|
+
if (detail.chatId != null) return;
|
|
19
|
+
if (detail.hasNewBelow) {
|
|
20
|
+
this._btn.classList.remove('hidden');
|
|
21
|
+
} else {
|
|
22
|
+
this._btn.classList.add('hidden');
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
window.addEventListener('claudeck:scroll-state', this._onState);
|
|
26
|
+
|
|
27
|
+
this._btn.addEventListener('click', async () => {
|
|
28
|
+
// Lazy import to avoid pulling parallel.js into component init.
|
|
29
|
+
const [{ getPane }, { scrollToBottom }] = await Promise.all([
|
|
30
|
+
import('../ui/parallel.js'),
|
|
31
|
+
import('../core/utils.js'),
|
|
32
|
+
]);
|
|
33
|
+
const pane = getPane(null);
|
|
34
|
+
if (pane) scrollToBottom(pane, { force: true });
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
disconnectedCallback() {
|
|
39
|
+
if (this._onState) window.removeEventListener('claudeck:scroll-state', this._onState);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
customElements.define('claudeck-jump-to-latest', JumpToLatest);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Web Component: Queue Stop Confirmation Dialog
|
|
2
|
+
class ClaudeckQueueStop extends HTMLElement {
|
|
3
|
+
connectedCallback() {
|
|
4
|
+
this.innerHTML = `
|
|
5
|
+
<div id="queue-stop-modal" class="modal-overlay hidden" data-persistent>
|
|
6
|
+
<div class="modal queue-stop-modal">
|
|
7
|
+
<div class="modal-header">
|
|
8
|
+
<h3>Queue Active</h3>
|
|
9
|
+
</div>
|
|
10
|
+
<p class="queue-stop-text">You have queued messages. What would you like to do?</p>
|
|
11
|
+
<div class="mq-queue-preview" id="queue-stop-preview"></div>
|
|
12
|
+
<div class="modal-actions queue-stop-actions">
|
|
13
|
+
<button id="queue-stop-all" class="modal-btn-cancel queue-stop-terminate">Terminate All</button>
|
|
14
|
+
<button id="queue-stop-skip" class="modal-btn-cancel">Skip to Next</button>
|
|
15
|
+
<button id="queue-stop-pause" class="modal-btn-save">Just Stop Current</button>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
`;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
customElements.define('claudeck-queue-stop', ClaudeckQueueStop);
|
package/public/js/core/api.js
CHANGED
|
@@ -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() {
|
package/public/js/core/dom.js
CHANGED
|
@@ -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
|
};
|
package/public/js/core/utils.js
CHANGED
|
@@ -20,6 +20,42 @@ export function getToolDetail(name, input) {
|
|
|
20
20
|
return "";
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
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":
|