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
@@ -6,6 +6,7 @@ import { renderMarkdown, highlightCodeBlocks, addCopyButtons } from '../ui/forma
6
6
  import * as api from '../core/api.js';
7
7
  import { getSelectedModel } from '../ui/model-selector.js';
8
8
  import { $ } from '../core/dom.js';
9
+ import { getSetting } from '../components/settings-modal.js';
9
10
 
10
11
  const SESSIONS_KEY = 'claudeck-bot-sessions';
11
12
  let panel, messagesDiv, inputEl, sendBtn, stopBtn, settingsOverlay, promptTextarea;
@@ -433,12 +434,27 @@ function newBotSession() {
433
434
  showBotWhaly();
434
435
  }
435
436
 
437
+ // ── Visibility ──────────────────────────────────────────
438
+
439
+ let bubble; // reference set in createBotDOM
440
+
441
+ function setBotVisible(visible) {
442
+ if (!bubble) return;
443
+ bubble.style.display = visible ? '' : 'none';
444
+ if (!visible && panel) closePanel();
445
+ }
446
+
436
447
  // ── Init ────────────────────────────────────────────────
437
448
 
438
449
  function init() {
439
450
  createBotDOM();
451
+ bubble = document.querySelector('.bot-bubble');
440
452
  on('ws:message', handleBotWsMessage);
441
453
  fetchSystemPrompt();
454
+
455
+ // Respect enable/disable setting
456
+ setBotVisible(getSetting('assistantBot', true));
457
+ window.addEventListener('setting:assistantBot', (e) => setBotVisible(e.detail));
442
458
  }
443
459
 
444
460
  // Run on import
@@ -147,7 +147,7 @@ registerTab({
147
147
  <li>Existing shortcuts (e.g. <code>openRightPanel('my-tab')</code>) work automatically</li>
148
148
  </ul>
149
149
 
150
- <div class="callout">See <code>plugins/event-stream/client.js</code> for a complete working example of a plugin tab.</div>
150
+ <div class="callout">See <code>plugins/claude-editor/client.js</code> for a complete working example of a plugin tab.</div>
151
151
  `,
152
152
  });
153
153
 
@@ -301,7 +301,7 @@ registerDocSection({
301
301
  <li>Import paths: use absolute paths (e.g. <code>/js/ui/tab-sdk.js</code>)</li>
302
302
  </ul>
303
303
 
304
- <div class="callout">When in doubt, look at <code>plugins/event-stream/</code>, <code>plugins/repos/</code>, or <code>plugins/tasks/</code> as reference implementations. For full-stack with server routes, see <code>plugins/linear/</code>.</div>
304
+ <div class="callout">When in doubt, look at <code>plugins/claude-editor/</code> or <code>plugins/repos/</code> as reference implementations. For full-stack 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
 
@@ -109,6 +109,7 @@ function renderMemories() {
109
109
  try {
110
110
  await fetchApi(`/${m.id}`, { method: 'DELETE' });
111
111
  memories = memories.filter(x => x.id !== m.id);
112
+ if ($.memoryTitle) $.memoryTitle.textContent = `Memory (${memories.length})`;
112
113
  renderMemories();
113
114
  loadStats();
114
115
  } catch { /* ignore */ }
@@ -5,11 +5,13 @@ import { $ } from '../core/dom.js';
5
5
  const sbGaugeSep = document.getElementById("sb-gauge-sep");
6
6
 
7
7
  const MODEL_LIMITS = {
8
+ opus: 1_000_000,
8
9
  default: 200_000,
9
10
  };
10
11
 
11
12
  function getLimit() {
12
- return MODEL_LIMITS.default;
13
+ const model = $.modelSelect?.value || '';
14
+ return MODEL_LIMITS[model] || MODEL_LIMITS.default;
13
15
  }
14
16
 
15
17
  function formatTokens(n) {
@@ -73,6 +75,13 @@ export function resetContextGauge() {
73
75
  if (sbGaugeSep) sbGaugeSep.classList.add('hidden');
74
76
  }
75
77
 
78
+ // Re-render gauge when model changes (limit may differ)
79
+ $.modelSelect?.addEventListener('change', () => {
80
+ const tokens = getState('sessionTokens');
81
+ const total = tokens.input + tokens.output + tokens.cacheRead + tokens.cacheCreation;
82
+ if (total > 0) renderGauge(tokens);
83
+ });
84
+
76
85
  export async function loadContextGauge(sessionId) {
77
86
  if (!sessionId) return;
78
87
  try {
@@ -31,21 +31,37 @@ function getLangLabel(lang) {
31
31
  export function renderMarkdown(text) {
32
32
  let html = escapeHtml(text);
33
33
 
34
- // ── Code blocks — with language header ──
34
+ // ── Placeholder system ──
35
+ // Extract code blocks and inline code into placeholders FIRST to protect
36
+ // their content from text-level regex passes (bold, italic, links, etc.)
37
+ const placeholders = [];
38
+ function placeholder(content) {
39
+ placeholders.push(content);
40
+ return `\x00PH${placeholders.length - 1}\x00`;
41
+ }
42
+
43
+ // ── Code blocks — extract to placeholders ──
35
44
  html = html.replace(
36
45
  /```(\w*)\n([\s\S]*?)```/g,
37
46
  (_, lang, code) => {
38
47
  const langClass = lang ? `language-${lang}` : "";
39
48
  const label = getLangLabel(lang);
40
49
  const headerHtml = label
41
- ? `<div class="code-block-header"><span class="code-lang-label">${escapeHtml(label)}</span></div>`
50
+ ? `<div class="code-block-header"><span class="code-lang-label">${label}</span></div>`
42
51
  : "";
43
- return `<div class="code-block-wrapper">${headerHtml}<pre><code class="${langClass}" data-lang="${lang}">${code}</code></pre></div>`;
52
+ const wrappedCode = code
53
+ .replace(/\n$/, "")
54
+ .split("\n")
55
+ .map(line => `<span class="code-line">${line}</span>`)
56
+ .join("\n");
57
+ return placeholder(`<div class="code-block-wrapper">${headerHtml}<pre><code class="${langClass}" data-lang="${lang}">${wrappedCode}</code></pre></div>`);
44
58
  }
45
59
  );
46
60
 
47
- // ── Inline code ──
48
- html = html.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>');
61
+ // ── Inline code — extract to placeholders ──
62
+ html = html.replace(/`([^`]+)`/g, (_, code) => {
63
+ return placeholder(`<code class="inline-code">${code}</code>`);
64
+ });
49
65
 
50
66
  // ── Bold + Italic combined ──
51
67
  html = html.replace(/\*\*\*(.+?)\*\*\*/g, "<strong><em>$1</em></strong>");
@@ -69,7 +85,6 @@ export function renderMarkdown(text) {
69
85
  html = html.replace(/^---+$/gm, '<hr class="md-hr">');
70
86
 
71
87
  // ── Blockquotes ──
72
- // Match consecutive lines starting with >
73
88
  html = html.replace(/(?:^&gt; (.*)$\n?)+/gm, (match) => {
74
89
  const lines = match.trim().split("\n").map(l => l.replace(/^&gt; ?/, "")).join("<br>");
75
90
  return `<blockquote class="md-blockquote">${lines}</blockquote>\n`;
@@ -78,8 +93,13 @@ export function renderMarkdown(text) {
78
93
  // ── Links ──
79
94
  html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" class="md-link" target="_blank" rel="noopener">$1</a>');
80
95
 
96
+ // ── Auto-link bare URLs (not already inside an <a> tag or href attribute) ──
97
+ html = html.replace(
98
+ /(?<!href="|">)(https?:\/\/[^\s<>"'`)\]]+)/g,
99
+ '<a href="$1" class="md-link" target="_blank" rel="noopener">$1</a>'
100
+ );
101
+
81
102
  // ── Tables ──
82
- // Match table blocks: header row, separator row, then data rows
83
103
  html = html.replace(
84
104
  /(?:^\|(.+)\|$\n^\|[-| :]+\|$\n(?:^\|(.+)\|$\n?)*)/gm,
85
105
  (match) => {
@@ -89,7 +109,6 @@ export function renderMarkdown(text) {
89
109
  const parseRow = (row) =>
90
110
  row.split("|").filter((_, i, arr) => i > 0 && i < arr.length - 1).map(c => c.trim());
91
111
 
92
- // Parse alignment from separator row
93
112
  const sepCells = parseRow(rows[1]);
94
113
  const aligns = sepCells.map(c => {
95
114
  if (c.startsWith(":") && c.endsWith(":")) return "center";
@@ -119,22 +138,43 @@ export function renderMarkdown(text) {
119
138
  );
120
139
 
121
140
  // ── Ordered lists ──
122
- // Match consecutive lines starting with digits followed by . or )
123
141
  html = html.replace(/(?:^\d+[.)]\s+.+$\n?)+/gm, (match) => {
124
142
  const items = match.trim().split("\n").map(l => l.replace(/^\d+[.)]\s+/, ""));
125
143
  return '<ol class="md-list md-ol">' + items.map(i => `<li>${i}</li>`).join("") + "</ol>\n";
126
144
  });
127
145
 
146
+ // ── Task lists (before general unordered lists) ──
147
+ html = html.replace(/(?:^[-*+]\s+\[[ xX]\]\s+.+$\n?)+/gm, (match) => {
148
+ const items = match.trim().split("\n").map(l => {
149
+ const checked = /^[-*+]\s+\[x\]/i.test(l);
150
+ const text = l.replace(/^[-*+]\s+\[[ xX]\]\s+/, "");
151
+ const checkbox = `<input type="checkbox" class="md-checkbox" ${checked ? "checked" : ""} disabled>`;
152
+ const spanClass = checked ? ' class="task-text-done"' : "";
153
+ return `<li>${checkbox}<span${spanClass}>${text}</span></li>`;
154
+ });
155
+ return '<ul class="md-list md-task-list">' + items.join("") + "</ul>\n";
156
+ });
157
+
128
158
  // ── Unordered lists ──
129
- // Match consecutive lines starting with -, *, or +
130
159
  html = html.replace(/(?:^[-*+]\s+.+$\n?)+/gm, (match) => {
131
160
  const items = match.trim().split("\n").map(l => l.replace(/^[-*+]\s+/, ""));
132
161
  return '<ul class="md-list md-ul">' + items.map(i => `<li>${i}</li>`).join("") + "</ul>\n";
133
162
  });
134
163
 
135
164
  // ── Line breaks ──
165
+ html = html.replace(/\n{3,}/g, "\n\n");
136
166
  html = html.replace(/\n/g, "<br>");
137
167
 
168
+ // Remove redundant <br> around block elements that already have CSS margins
169
+ html = html.replace(/(<br>)+(<(?:h[1-4]|ul|ol|div|table|blockquote|hr)[\s>])/g, "$2");
170
+ html = html.replace(/(<\/(?:h[1-4]|ul|ol|div|table|blockquote|hr)>)(<br>)+/g, "$1");
171
+ // Also clean <br> around placeholder tokens (code blocks are block-level)
172
+ html = html.replace(/(<br>)+(\x00PH\d+\x00)/g, "$2");
173
+ html = html.replace(/(\x00PH\d+\x00)(<br>)+/g, "$1");
174
+
175
+ // ── Restore placeholders ──
176
+ html = html.replace(/\x00PH(\d+)\x00/g, (_, i) => placeholders[parseInt(i)]);
177
+
138
178
  return html;
139
179
  }
140
180
 
@@ -143,9 +183,23 @@ export function highlightCodeBlocks(container) {
143
183
  container.querySelectorAll("pre code").forEach((block) => {
144
184
  if (block.dataset.highlighted === "yes") return;
145
185
  try {
146
- // Highlight both language-tagged and untagged blocks (auto-detect)
147
186
  hljs.highlightElement(block);
148
187
  } catch { /* ignore unsupported languages */ }
188
+ // Re-wrap lines for CSS line numbering after highlight.js processes the block
189
+ wrapCodeLinesInBlock(block);
190
+ });
191
+ }
192
+
193
+ function wrapCodeLinesInBlock(block) {
194
+ if (block.querySelector(".code-line")) return;
195
+ const html = block.innerHTML;
196
+ const lines = html.split("\n");
197
+ block.innerHTML = lines.map(l => `<span class="code-line">${l}</span>`).join("\n");
198
+ }
199
+
200
+ export function wrapCodeLines(container) {
201
+ container.querySelectorAll("pre code").forEach((block) => {
202
+ wrapCodeLinesInBlock(block);
149
203
  });
150
204
  }
151
205
 
@@ -70,3 +70,33 @@ document.addEventListener("keydown", (e) => {
70
70
  document.querySelectorAll(".header-dropdown.open").forEach((d) => d.classList.remove("open"));
71
71
  }
72
72
  });
73
+
74
+ // Sync header dropdown display when hidden selects change programmatically
75
+ function syncDropdownDisplay(selectId) {
76
+ const select = document.getElementById(selectId);
77
+ if (!select) return;
78
+
79
+ function sync() {
80
+ const val = select.value;
81
+ const items = document.querySelectorAll(`.header-submenu-item[data-target="${selectId}"]`);
82
+ let matchedText = null;
83
+ items.forEach((item) => {
84
+ const isMatch = item.dataset.value === val;
85
+ item.classList.toggle("active", isMatch);
86
+ if (isMatch) matchedText = item.textContent.trim();
87
+ });
88
+ if (matchedText) {
89
+ const parent = items[0]?.closest(".header-dropdown-item");
90
+ const display = parent?.querySelector(".header-dropdown-item-value");
91
+ if (display) display.textContent = matchedText;
92
+ }
93
+ }
94
+
95
+ select.addEventListener("change", sync);
96
+ // Initial sync for values restored from localStorage
97
+ sync();
98
+ }
99
+
100
+ syncDropdownDisplay("model-select");
101
+ syncDropdownDisplay("perm-mode-select");
102
+ syncDropdownDisplay("max-turns-select");
@@ -6,16 +6,23 @@ const elPerm = document.getElementById("input-meta-perm");
6
6
  const elTurns = document.getElementById("input-meta-turns");
7
7
 
8
8
  const permLabels = {
9
- bypass: "bypass",
10
- confirmDangerous: "confirm dangerous",
11
- confirmAll: "confirm all",
12
- plan: "plan only",
9
+ bypass: "Bypass",
10
+ confirmDangerous: "Confirm Writes",
11
+ confirmAll: "Confirm All",
12
+ plan: "Plan Mode",
13
+ };
14
+
15
+ const modelLabels = {
16
+ "": "Auto",
17
+ sonnet: "Sonnet",
18
+ opus: "Opus",
19
+ haiku: "Haiku",
13
20
  };
14
21
 
15
22
  function updateModel() {
16
23
  if (!elModel) return;
17
24
  const val = $.modelSelect?.value || "";
18
- elModel.textContent = val || "default model";
25
+ elModel.textContent = modelLabels[val] || val || "Auto";
19
26
  }
20
27
 
21
28
  function updatePerm() {
@@ -27,7 +34,7 @@ function updatePerm() {
27
34
  function updateTurns() {
28
35
  if (!elTurns) return;
29
36
  const val = $.maxTurnsSelect?.value || "30";
30
- elTurns.textContent = val === "0" ? "unlimited turns" : `${val} turns`;
37
+ elTurns.textContent = val === "0" ? "Unlimited" : `${val} turns`;
31
38
  }
32
39
 
33
40
  // Also watch header dropdown display elements (used when dropdowns replace <select>)
@@ -9,13 +9,16 @@ export function getMaxTurns() {
9
9
  }
10
10
 
11
11
  function init() {
12
+ $.maxTurnsSelect?.addEventListener('change', () => {
13
+ localStorage.setItem(STORAGE_KEY, $.maxTurnsSelect.value);
14
+ });
12
15
  const saved = localStorage.getItem(STORAGE_KEY);
13
16
  if (saved && $.maxTurnsSelect) {
14
17
  $.maxTurnsSelect.value = saved;
18
+ queueMicrotask(() => {
19
+ $.maxTurnsSelect?.dispatchEvent(new Event('change', { bubbles: true }));
20
+ });
15
21
  }
16
- $.maxTurnsSelect?.addEventListener('change', () => {
17
- localStorage.setItem(STORAGE_KEY, $.maxTurnsSelect.value);
18
- });
19
22
  }
20
23
 
21
24
  init();
@@ -6,9 +6,89 @@ import { getState, setState } from '../core/store.js';
6
6
  import { $ } from '../core/dom.js';
7
7
  import { getPane } from './parallel.js';
8
8
 
9
+ const WELCOME_GREETINGS = [
10
+ "What can I help you build today?",
11
+ "Ready when you are!",
12
+ "Let's create something great together",
13
+ "What's on your mind?",
14
+ "Let's turn your ideas into code",
15
+ "What are we working on today?",
16
+ "Got a bug to squash or a feature to ship?",
17
+ "Your next big idea starts here",
18
+ "Let's make something awesome",
19
+ "How can I help you today?",
20
+ "What challenge are we tackling today?",
21
+ "Let's get things done together",
22
+ ];
23
+
24
+ function getRandomGreeting() {
25
+ return WELCOME_GREETINGS[Math.floor(Math.random() * WELCOME_GREETINGS.length)];
26
+ }
27
+
28
+ function getChatAreaMain() {
29
+ return document.querySelector('.chat-area-main');
30
+ }
31
+
32
+ export function showWelcomeState() {
33
+ const chatMain = getChatAreaMain();
34
+ if (!chatMain) return;
35
+ // Create the welcome center element if it doesn't exist
36
+ let welcomeEl = chatMain.querySelector('.welcome-center');
37
+ if (!welcomeEl) {
38
+ welcomeEl = document.createElement('div');
39
+ welcomeEl.className = 'welcome-center';
40
+ welcomeEl.innerHTML = `<img class="whaly-welcome-img" src="/icons/whaly.png" alt="Whaly" draggable="false"><div class="welcome-greeting">${getRandomGreeting()}</div>`;
41
+ // Insert before the input-bar
42
+ const inputBar = chatMain.querySelector('.input-bar');
43
+ if (inputBar) {
44
+ chatMain.insertBefore(welcomeEl, inputBar);
45
+ } else {
46
+ chatMain.appendChild(welcomeEl);
47
+ }
48
+ } else {
49
+ // Update greeting text
50
+ const greetingEl = welcomeEl.querySelector('.welcome-greeting');
51
+ if (greetingEl) greetingEl.textContent = getRandomGreeting();
52
+ }
53
+ chatMain.classList.remove('welcome-exit');
54
+ chatMain.classList.add('welcome-state');
55
+ }
56
+
57
+ export function exitWelcomeState() {
58
+ const chatMain = getChatAreaMain();
59
+ if (!chatMain || !chatMain.classList.contains('welcome-state')) return Promise.resolve();
60
+
61
+ return new Promise(resolve => {
62
+ chatMain.classList.add('welcome-exit');
63
+
64
+ const onEnd = () => {
65
+ chatMain.classList.remove('welcome-state', 'welcome-exit');
66
+ resolve();
67
+ };
68
+ // Listen for the fade-out animation on the welcome center
69
+ const welcomeEl = chatMain.querySelector('.welcome-center');
70
+ if (welcomeEl) {
71
+ welcomeEl.addEventListener('animationend', onEnd, { once: true });
72
+ } else {
73
+ onEnd();
74
+ }
75
+ });
76
+ }
77
+
78
+ export function isWelcomeStateActive() {
79
+ const chatMain = getChatAreaMain();
80
+ return chatMain?.classList.contains('welcome-state') || false;
81
+ }
82
+
9
83
  export function showWhalyPlaceholder(pane) {
10
84
  pane = pane || getPane(null);
11
85
  removeWhalyPlaceholder(pane);
86
+ // Use welcome state for the main (non-parallel) pane when the DOM supports it
87
+ const parallelMode = getState("parallelMode");
88
+ if (!parallelMode && getChatAreaMain()) {
89
+ showWelcomeState();
90
+ }
91
+ // Always add the placeholder into the messages div (tests + parallel mode rely on this)
12
92
  const el = document.createElement("div");
13
93
  el.className = "whaly-placeholder";
14
94
  el.innerHTML = `<img src="/icons/whaly.png" alt="Whaly" draggable="false"><div class="whaly-text">~ start chatting with claude ~</div><div class="whaly-hint">Type a message or select a prompt template</div>`;
@@ -24,6 +104,9 @@ export function removeWhalyPlaceholder(pane) {
24
104
  export function addUserMessage(text, pane, images = [], filePaths = []) {
25
105
  pane = pane || getPane(null);
26
106
  removeWhalyPlaceholder(pane);
107
+ // Exit welcome state immediately (no animation — message renders right away)
108
+ const chatMain = getChatAreaMain();
109
+ if (chatMain) chatMain.classList.remove('welcome-state', 'welcome-exit');
27
110
  pane.currentAssistantMsg = null;
28
111
  const div = document.createElement("div");
29
112
  div.className = "msg msg-user";
@@ -58,7 +141,8 @@ export function addUserMessage(text, pane, images = [], filePaths = []) {
58
141
  }
59
142
 
60
143
  pane.messagesDiv.appendChild(div);
61
- scrollToBottom(pane);
144
+ // User pressed send — always pull them to the bottom regardless of scroll position.
145
+ scrollToBottom(pane, { force: true });
62
146
  }
63
147
 
64
148
  function renderChatImages(images, container) {
@@ -343,6 +427,9 @@ export function renderMessagesIntoPane(messages, pane) {
343
427
  showWhalyPlaceholder(pane);
344
428
  return;
345
429
  }
430
+ // Exit welcome state when loading existing messages
431
+ const chatMain = getChatAreaMain();
432
+ if (chatMain) chatMain.classList.remove('welcome-state', 'welcome-exit');
346
433
  // Track last assistant message ID for fork button placement
347
434
  let lastAssistantMsgEl = null;
348
435
  let lastAssistantMsgId = null;
@@ -432,6 +519,15 @@ export function renderMessagesIntoPane(messages, pane) {
432
519
  highlightCodeBlocks(pane.messagesDiv);
433
520
  addCopyButtons(pane.messagesDiv);
434
521
  renderMermaidBlocks(pane.messagesDiv);
522
+ // Loading a saved session: jump to the latest message and re-engage follow mode.
523
+ // Per-message scrollToBottom calls during the render loop are no-ops because
524
+ // scrollTop=0 + a tall scrollHeight fails the near-bottom check; the final
525
+ // forced scroll lands the user where they expect (newest message visible).
526
+ // Skip when rendering into a detached temp container (e.g. prependOlderMessages),
527
+ // which only uses this function for DOM rendering and manages scroll itself.
528
+ if (pane.messagesDiv && pane.messagesDiv.isConnected) {
529
+ scrollToBottom(pane, { force: true });
530
+ }
435
531
  }
436
532
 
437
533
  function addForkButton(msgEl, messageId) {
@@ -11,6 +11,7 @@ function init() {
11
11
  const saved = localStorage.getItem(STORAGE_KEY);
12
12
  if (saved && $.modelSelect) {
13
13
  $.modelSelect.value = saved;
14
+ $.modelSelect.dispatchEvent(new Event('change', { bubbles: true }));
14
15
  }
15
16
  $.modelSelect?.addEventListener('change', () => {
16
17
  localStorage.setItem(STORAGE_KEY, $.modelSelect.value);
@@ -4,6 +4,26 @@ import { getState, setState } from '../core/store.js';
4
4
  import { CHAT_IDS } from '../core/constants.js';
5
5
  import { handleAutocompleteKeydown, handleSlashAutocomplete } from './commands.js';
6
6
  import { handleHistoryKeydown } from '../features/input-history.js';
7
+ import { isNearBottom } from '../core/utils.js';
8
+
9
+ // Wire stick-to-bottom tracking onto a pane. Updates pane.followBottom as the
10
+ // user scrolls so scrollToBottom() knows whether to yank them or leave them be.
11
+ function attachScrollTracking(pane) {
12
+ pane.followBottom = true;
13
+ pane.hasNewBelow = false;
14
+ const el = pane.messagesDiv;
15
+ if (!el || !el.addEventListener) return;
16
+ el.addEventListener("scroll", () => {
17
+ const atBottom = isNearBottom(el);
18
+ pane.followBottom = atBottom;
19
+ if (atBottom && pane.hasNewBelow) {
20
+ pane.hasNewBelow = false;
21
+ window.dispatchEvent(new CustomEvent("claudeck:scroll-state", {
22
+ detail: { chatId: pane.chatId, hasNewBelow: false },
23
+ }));
24
+ }
25
+ }, { passive: true });
26
+ }
7
27
 
8
28
  // Panes map — chatId -> pane state object
9
29
  export const panes = new Map();
@@ -15,7 +35,7 @@ export function getPane(chatId) {
15
35
 
16
36
  export function initSinglePane() {
17
37
  panes.clear();
18
- panes.set(null, {
38
+ const pane = {
19
39
  chatId: null,
20
40
  messagesDiv: $.messagesDiv,
21
41
  messageInput: $.messageInput,
@@ -25,7 +45,12 @@ export function initSinglePane() {
25
45
  currentAssistantMsg: null,
26
46
  autocompleteEl: document.getElementById("slash-autocomplete"),
27
47
  _autocompleteIndex: -1,
28
- });
48
+ _messageQueue: [],
49
+ _queuePaused: false,
50
+ _queuePauseReason: null,
51
+ };
52
+ attachScrollTracking(pane);
53
+ panes.set(null, pane);
29
54
  }
30
55
 
31
56
  // Initialize on load
@@ -88,8 +113,13 @@ export function createChatPane(chatId, index) {
88
113
  statusEl: header.querySelector(".chat-pane-status"),
89
114
  autocompleteEl: paneAutocomplete,
90
115
  _autocompleteIndex: -1,
116
+ _messageQueue: [],
117
+ _queuePaused: false,
118
+ _queuePauseReason: null,
91
119
  };
92
120
 
121
+ attachScrollTracking(state);
122
+
93
123
  paneSendBtn.addEventListener("click", () => sendMessage(state));
94
124
  paneStopBtn.addEventListener("click", () => stopGeneration(state));
95
125
 
@@ -153,6 +153,7 @@ function initPermissions() {
153
153
  const saved = localStorage.getItem(STORAGE_KEY);
154
154
  if (saved && $.permModeSelect) {
155
155
  $.permModeSelect.value = saved;
156
+ $.permModeSelect.dispatchEvent(new Event('change', { bubbles: true }));
156
157
  }
157
158
 
158
159
  // Persist mode changes
@@ -6,7 +6,6 @@ import { initTabSDK } from "./tab-sdk.js";
6
6
  const STORAGE_KEY = "claudeck-right-panel";
7
7
  const TAB_KEY = "claudeck-right-panel-tab";
8
8
  const WIDTH_KEY = "claudeck-right-panel-width";
9
- const OLD_LINEAR_KEY = "claudeck-linear-panel";
10
9
  const MIN_WIDTH = 200;
11
10
  const MAX_WIDTH_RATIO = 0.6; // 60vw
12
11
 
@@ -82,13 +81,6 @@ function applyTab(tabName) {
82
81
  }
83
82
 
84
83
  function initRightPanel() {
85
- // Migrate old linear panel localStorage key
86
- const oldState = localStorage.getItem(OLD_LINEAR_KEY);
87
- if (oldState && !localStorage.getItem(STORAGE_KEY)) {
88
- localStorage.setItem(STORAGE_KEY, oldState);
89
- localStorage.removeItem(OLD_LINEAR_KEY);
90
- }
91
-
92
84
  // Tab click handlers
93
85
  $.rightPanel.querySelectorAll(".right-panel-tab").forEach((btn) => {
94
86
  btn.addEventListener("click", () => {