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.
- package/README.md +6 -8
- package/package.json +1 -1
- package/plugins/claude-editor/manifest.json +10 -0
- package/plugins/repos/manifest.json +10 -0
- 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/right-panel.css +207 -0
- package/public/css/ui/sessions.css +173 -0
- package/public/css/ui/settings.css +75 -0
- package/public/index.html +10 -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/components/settings-modal.js +65 -0
- package/public/js/core/api.js +15 -43
- package/public/js/core/dom.js +17 -0
- package/public/js/core/events.js +11 -0
- package/public/js/core/plugin-loader.js +96 -11
- package/public/js/core/store.js +11 -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 +4 -1
- package/public/js/panels/assistant-bot.js +16 -0
- package/public/js/panels/dev-docs.js +2 -2
- package/public/js/panels/memory.js +1 -0
- package/public/js/ui/context-gauge.js +10 -1
- package/public/js/ui/formatting.js +65 -11
- package/public/js/ui/header-dropdowns.js +30 -0
- package/public/js/ui/input-meta.js +13 -6
- package/public/js/ui/max-turns.js +6 -3
- package/public/js/ui/messages.js +97 -1
- package/public/js/ui/model-selector.js +1 -0
- package/public/js/ui/parallel.js +32 -2
- package/public/js/ui/permissions.js +1 -0
- package/public/js/ui/right-panel.js +0 -8
- package/public/js/ui/tab-sdk.js +395 -176
- package/public/style.css +2 -0
- package/server/memory-optimizer.js +17 -13
- package/server/routes/marketplace.js +316 -0
- package/server/routes/projects.js +0 -0
- package/server/ws-handler.js +22 -15
- package/server.js +18 -0
- package/plugins/event-stream/client.css +0 -207
- package/plugins/event-stream/client.js +0 -271
- package/plugins/linear/client.css +0 -345
- package/plugins/linear/client.js +0 -380
- package/plugins/linear/config.json +0 -5
- package/plugins/linear/server.js +0 -312
- package/plugins/sudoku/client.css +0 -196
- package/plugins/sudoku/client.js +0 -329
- package/plugins/tasks/client.css +0 -414
- package/plugins/tasks/client.js +0 -394
- package/plugins/tasks/server.js +0 -116
- package/plugins/tic-tac-toe/client.css +0 -167
- package/plugins/tic-tac-toe/client.js +0 -241
- 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/
|
|
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/
|
|
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
|
-
|
|
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
|
-
// ──
|
|
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">${
|
|
50
|
+
? `<div class="code-block-header"><span class="code-lang-label">${label}</span></div>`
|
|
42
51
|
: "";
|
|
43
|
-
|
|
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,
|
|
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(/(?:^> (.*)$\n?)+/gm, (match) => {
|
|
74
89
|
const lines = match.trim().split("\n").map(l => l.replace(/^> ?/, "")).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: "
|
|
10
|
-
confirmDangerous: "
|
|
11
|
-
confirmAll: "
|
|
12
|
-
plan: "
|
|
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 ||
|
|
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" ? "
|
|
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();
|
package/public/js/ui/messages.js
CHANGED
|
@@ -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
|
-
|
|
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);
|
package/public/js/ui/parallel.js
CHANGED
|
@@ -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
|
-
|
|
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", () => {
|