claudeck 1.3.1 → 1.4.1
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 +13 -9
- package/db/sqlite.js +1697 -0
- package/db.js +3 -1645
- package/package.json +2 -1
- package/plugins/claude-editor/manifest.json +10 -0
- package/plugins/linear/manifest.json +10 -0
- package/plugins/repos/manifest.json +10 -0
- package/public/css/ui/messages.css +25 -0
- package/public/css/ui/right-panel.css +207 -0
- package/public/css/ui/settings.css +75 -0
- package/public/index.html +7 -0
- package/public/js/components/settings-modal.js +65 -0
- package/public/js/core/api.js +23 -6
- 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/ws.js +12 -0
- package/public/js/features/chat.js +4 -0
- package/public/js/features/sessions.js +102 -10
- package/public/js/main.js +1 -0
- 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/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 +42 -0
- package/public/js/ui/model-selector.js +1 -0
- package/public/js/ui/parallel.js +2 -4
- package/public/js/ui/permissions.js +1 -0
- package/public/js/ui/tab-sdk.js +395 -176
- package/public/style.css +1 -0
- package/server/agent-loop.js +26 -26
- package/server/memory-extractor.js +4 -4
- package/server/memory-injector.js +11 -11
- package/server/memory-optimizer.js +19 -15
- package/server/notification-logger.js +5 -5
- package/server/orchestrator.js +15 -15
- package/server/push-sender.js +2 -2
- package/server/routes/agents.js +2 -2
- package/server/routes/marketplace.js +316 -0
- package/server/routes/memory.js +20 -20
- package/server/routes/messages.js +41 -10
- package/server/routes/notifications.js +20 -20
- package/server/routes/sessions.js +17 -17
- package/server/routes/stats.js +37 -37
- package/server/routes/worktrees.js +9 -9
- package/server/summarizer.js +3 -3
- package/server/ws-handler.js +163 -58
- package/server.js +20 -2
- package/plugins/event-stream/client.css +0 -207
- package/plugins/event-stream/client.js +0 -271
- 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
|
@@ -5,8 +5,12 @@ import { CHAT_IDS } from '../core/constants.js';
|
|
|
5
5
|
import { escapeHtml } from '../core/utils.js';
|
|
6
6
|
import * as api from '../core/api.js';
|
|
7
7
|
import { panes, enterParallelMode, exitParallelMode } from '../ui/parallel.js';
|
|
8
|
-
import { renderMessagesIntoPane, showWhalyPlaceholder } from '../ui/messages.js';
|
|
8
|
+
import { renderMessagesIntoPane, prependOlderMessages, showWhalyPlaceholder, showLoadingIndicator, hideLoadingIndicator } from '../ui/messages.js';
|
|
9
9
|
import { loadContextGauge } from '../ui/context-gauge.js';
|
|
10
|
+
import { subscribeToSession } from '../core/ws.js';
|
|
11
|
+
|
|
12
|
+
const MESSAGE_PAGE_SIZE = 30;
|
|
13
|
+
const SCROLL_LOAD_THRESHOLD = 150; // px from top to trigger load more
|
|
10
14
|
|
|
11
15
|
const SESSION_STORAGE_KEY = "claudeck-session-id";
|
|
12
16
|
|
|
@@ -14,6 +18,7 @@ const SESSION_STORAGE_KEY = "claudeck-session-id";
|
|
|
14
18
|
onState("sessionId", (val) => {
|
|
15
19
|
if (val) {
|
|
16
20
|
localStorage.setItem(SESSION_STORAGE_KEY, val);
|
|
21
|
+
subscribeToSession(val);
|
|
17
22
|
} else {
|
|
18
23
|
localStorage.removeItem(SESSION_STORAGE_KEY);
|
|
19
24
|
}
|
|
@@ -228,16 +233,16 @@ export async function deleteSession(id) {
|
|
|
228
233
|
|
|
229
234
|
export async function loadMessages(sid) {
|
|
230
235
|
if (getState("parallelMode")) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
236
|
+
// Load all panes concurrently instead of sequentially
|
|
237
|
+
await Promise.all(CHAT_IDS.map(chatId => loadPaneMessages(sid, chatId)));
|
|
234
238
|
return;
|
|
235
239
|
}
|
|
236
240
|
|
|
237
241
|
const pane = panes.get(null);
|
|
238
242
|
try {
|
|
239
|
-
const messages = await api.fetchSingleMessages(sid);
|
|
243
|
+
const messages = await api.fetchSingleMessages(sid, { limit: MESSAGE_PAGE_SIZE });
|
|
240
244
|
renderMessagesIntoPane(messages, pane);
|
|
245
|
+
_initPanePagination(pane, messages, "single");
|
|
241
246
|
loadContextGauge(sid);
|
|
242
247
|
} catch (err) {
|
|
243
248
|
console.error("Failed to load messages:", err);
|
|
@@ -248,22 +253,109 @@ export async function loadPaneMessages(sid, chatId) {
|
|
|
248
253
|
const pane = panes.get(chatId);
|
|
249
254
|
if (!pane) return;
|
|
250
255
|
try {
|
|
251
|
-
let messages
|
|
252
|
-
|
|
256
|
+
let messages;
|
|
253
257
|
// For Chat 1 (chat-0): also load single-mode messages as fallback
|
|
254
258
|
if (chatId === CHAT_IDS[0]) {
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
259
|
+
const [chatMsgs, singleMsgs] = await Promise.all([
|
|
260
|
+
api.fetchMessagesByChatId(sid, chatId, { limit: MESSAGE_PAGE_SIZE }),
|
|
261
|
+
api.fetchSingleMessages(sid, { limit: MESSAGE_PAGE_SIZE }),
|
|
262
|
+
]);
|
|
263
|
+
if (singleMsgs.length > 0) {
|
|
264
|
+
messages = [...singleMsgs, ...chatMsgs].sort((a, b) => a.id - b.id);
|
|
265
|
+
} else {
|
|
266
|
+
messages = chatMsgs;
|
|
258
267
|
}
|
|
268
|
+
} else {
|
|
269
|
+
messages = await api.fetchMessagesByChatId(sid, chatId, { limit: MESSAGE_PAGE_SIZE });
|
|
259
270
|
}
|
|
260
271
|
|
|
261
272
|
renderMessagesIntoPane(messages, pane);
|
|
273
|
+
_initPanePagination(pane, messages, chatId === CHAT_IDS[0] ? "chat0" : "chat");
|
|
262
274
|
} catch (err) {
|
|
263
275
|
console.error(`Failed to load messages for ${chatId}:`, err);
|
|
264
276
|
}
|
|
265
277
|
}
|
|
266
278
|
|
|
279
|
+
// ── Lazy-load pagination ────────────────────────────────
|
|
280
|
+
|
|
281
|
+
function _initPanePagination(pane, messages, mode) {
|
|
282
|
+
pane._hasMore = messages.length >= MESSAGE_PAGE_SIZE;
|
|
283
|
+
pane._oldestMessageId = messages.length > 0 ? messages[0].id : null;
|
|
284
|
+
pane._loadingMore = false;
|
|
285
|
+
pane._paginationMode = mode; // "single" | "chat" | "chat0"
|
|
286
|
+
|
|
287
|
+
// Remove any existing scroll listener
|
|
288
|
+
if (pane._scrollHandler) {
|
|
289
|
+
pane.messagesDiv.removeEventListener("scroll", pane._scrollHandler);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (pane._hasMore) {
|
|
293
|
+
pane._scrollHandler = () => _onPaneScroll(pane);
|
|
294
|
+
pane.messagesDiv.addEventListener("scroll", pane._scrollHandler, { passive: true });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function _onPaneScroll(pane) {
|
|
299
|
+
if (
|
|
300
|
+
pane.messagesDiv.scrollTop < SCROLL_LOAD_THRESHOLD &&
|
|
301
|
+
pane._hasMore &&
|
|
302
|
+
!pane._loadingMore
|
|
303
|
+
) {
|
|
304
|
+
_loadMoreMessages(pane);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function _loadMoreMessages(pane) {
|
|
309
|
+
pane._loadingMore = true;
|
|
310
|
+
showLoadingIndicator(pane);
|
|
311
|
+
|
|
312
|
+
const sid = getState("sessionId");
|
|
313
|
+
const before = pane._oldestMessageId;
|
|
314
|
+
const opts = { limit: MESSAGE_PAGE_SIZE, before };
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
let olderMessages;
|
|
318
|
+
|
|
319
|
+
switch (pane._paginationMode) {
|
|
320
|
+
case "single":
|
|
321
|
+
olderMessages = await api.fetchSingleMessages(sid, opts);
|
|
322
|
+
break;
|
|
323
|
+
case "chat0": {
|
|
324
|
+
// Chat 1 merges chatId + single messages
|
|
325
|
+
const [chatMsgs, singleMsgs] = await Promise.all([
|
|
326
|
+
api.fetchMessagesByChatId(sid, pane.chatId, opts),
|
|
327
|
+
api.fetchSingleMessages(sid, opts),
|
|
328
|
+
]);
|
|
329
|
+
olderMessages = singleMsgs.length > 0
|
|
330
|
+
? [...singleMsgs, ...chatMsgs].sort((a, b) => a.id - b.id)
|
|
331
|
+
: chatMsgs;
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
default:
|
|
335
|
+
olderMessages = await api.fetchMessagesByChatId(sid, pane.chatId, opts);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (olderMessages.length === 0) {
|
|
339
|
+
pane._hasMore = false;
|
|
340
|
+
} else {
|
|
341
|
+
pane._oldestMessageId = olderMessages[0].id;
|
|
342
|
+
pane._hasMore = olderMessages.length >= MESSAGE_PAGE_SIZE;
|
|
343
|
+
prependOlderMessages(olderMessages, pane);
|
|
344
|
+
}
|
|
345
|
+
} catch (err) {
|
|
346
|
+
console.error("Failed to load more messages:", err);
|
|
347
|
+
} finally {
|
|
348
|
+
hideLoadingIndicator(pane);
|
|
349
|
+
pane._loadingMore = false;
|
|
350
|
+
|
|
351
|
+
// Detach scroll listener if no more messages
|
|
352
|
+
if (!pane._hasMore && pane._scrollHandler) {
|
|
353
|
+
pane.messagesDiv.removeEventListener("scroll", pane._scrollHandler);
|
|
354
|
+
pane._scrollHandler = null;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
267
359
|
// ── Session Context Menu ────────────────────────────────
|
|
268
360
|
let sessionCtxMenu = null;
|
|
269
361
|
|
package/public/js/main.js
CHANGED
|
@@ -18,6 +18,7 @@ import './components/permission-modal.js';
|
|
|
18
18
|
import './components/linear-create-modal.js';
|
|
19
19
|
import './components/telegram-modal.js';
|
|
20
20
|
import './components/mcp-modal.js';
|
|
21
|
+
import './components/settings-modal.js';
|
|
21
22
|
import './components/add-project-modal.js';
|
|
22
23
|
import './components/status-bar.js';
|
|
23
24
|
|
|
@@ -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 with server routes, see <code>plugins/linear/</code>. More examples are available in the <a href="https://github.com/hamedafarag/claudeck-marketplace" target="_blank">marketplace repo</a>.</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 {
|
|
@@ -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
|
@@ -443,3 +443,45 @@ function addForkButton(msgEl, messageId) {
|
|
|
443
443
|
btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>`;
|
|
444
444
|
msgEl.appendChild(btn);
|
|
445
445
|
}
|
|
446
|
+
|
|
447
|
+
// ── Lazy-loading helpers ────────────────────────────────
|
|
448
|
+
|
|
449
|
+
export function prependOlderMessages(messages, pane) {
|
|
450
|
+
if (!messages || messages.length === 0) return;
|
|
451
|
+
|
|
452
|
+
// Render older messages into a detached container using the same rendering logic
|
|
453
|
+
const tempContainer = document.createElement("div");
|
|
454
|
+
const tempPane = { messagesDiv: tempContainer, currentAssistantMsg: null };
|
|
455
|
+
renderMessagesIntoPane(messages, tempPane);
|
|
456
|
+
|
|
457
|
+
// Capture scroll position before DOM mutation
|
|
458
|
+
const scrollHeightBefore = pane.messagesDiv.scrollHeight;
|
|
459
|
+
|
|
460
|
+
// Move all rendered nodes into the real pane
|
|
461
|
+
const fragment = document.createDocumentFragment();
|
|
462
|
+
while (tempContainer.firstChild) {
|
|
463
|
+
fragment.appendChild(tempContainer.firstChild);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Insert after loading indicator (if present) or at the top
|
|
467
|
+
const indicator = pane.messagesDiv.querySelector(".load-more-indicator");
|
|
468
|
+
const insertRef = indicator ? indicator.nextSibling : pane.messagesDiv.firstChild;
|
|
469
|
+
pane.messagesDiv.insertBefore(fragment, insertRef);
|
|
470
|
+
|
|
471
|
+
// Restore scroll position so the user's view doesn't jump
|
|
472
|
+
const scrollHeightAfter = pane.messagesDiv.scrollHeight;
|
|
473
|
+
pane.messagesDiv.scrollTop += (scrollHeightAfter - scrollHeightBefore);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export function showLoadingIndicator(pane) {
|
|
477
|
+
if (pane.messagesDiv.querySelector(".load-more-indicator")) return;
|
|
478
|
+
const el = document.createElement("div");
|
|
479
|
+
el.className = "load-more-indicator";
|
|
480
|
+
el.innerHTML = '<span class="load-more-spinner"></span> Loading older messages\u2026';
|
|
481
|
+
pane.messagesDiv.prepend(el);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export function hideLoadingIndicator(pane) {
|
|
485
|
+
const el = pane.messagesDiv.querySelector(".load-more-indicator");
|
|
486
|
+
if (el) el.remove();
|
|
487
|
+
}
|
|
@@ -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
|
@@ -138,11 +138,9 @@ export function enterParallelMode() {
|
|
|
138
138
|
|
|
139
139
|
const sessionId = getState("sessionId");
|
|
140
140
|
if (sessionId) {
|
|
141
|
-
// Lazy import to avoid circular dependency
|
|
141
|
+
// Lazy import to avoid circular dependency — load all panes concurrently
|
|
142
142
|
import('../features/sessions.js').then(({ loadPaneMessages }) => {
|
|
143
|
-
|
|
144
|
-
loadPaneMessages(sessionId, chatId);
|
|
145
|
-
}
|
|
143
|
+
Promise.all(CHAT_IDS.map(chatId => loadPaneMessages(sessionId, chatId)));
|
|
146
144
|
});
|
|
147
145
|
}
|
|
148
146
|
}
|
|
@@ -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
|