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