clay-server 2.27.0-beta.8 → 2.27.0

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 (72) hide show
  1. package/README.md +10 -0
  2. package/lib/daemon-projects.js +164 -0
  3. package/lib/daemon.js +13 -126
  4. package/lib/mates-identity.js +132 -0
  5. package/lib/mates-knowledge.js +113 -0
  6. package/lib/mates-prompts.js +398 -0
  7. package/lib/mates.js +40 -599
  8. package/lib/project-connection.js +2 -0
  9. package/lib/project-debate.js +19 -12
  10. package/lib/project-http.js +4 -2
  11. package/lib/project-loop.js +110 -48
  12. package/lib/project-mate-interaction.js +4 -0
  13. package/lib/project-notifications.js +210 -0
  14. package/lib/project-sessions.js +5 -2
  15. package/lib/project-user-message.js +2 -1
  16. package/lib/project.js +26 -2
  17. package/lib/public/app.js +1193 -8521
  18. package/lib/public/css/command-palette.css +14 -0
  19. package/lib/public/css/loop.css +301 -0
  20. package/lib/public/css/notifications-center.css +190 -0
  21. package/lib/public/css/rewind.css +6 -0
  22. package/lib/public/index.html +89 -35
  23. package/lib/public/modules/app-connection.js +160 -0
  24. package/lib/public/modules/app-cursors.js +473 -0
  25. package/lib/public/modules/app-debate-ui.js +389 -0
  26. package/lib/public/modules/app-dm.js +627 -0
  27. package/lib/public/modules/app-favicon.js +212 -0
  28. package/lib/public/modules/app-header.js +229 -0
  29. package/lib/public/modules/app-home-hub.js +600 -0
  30. package/lib/public/modules/app-loop-ui.js +589 -0
  31. package/lib/public/modules/app-loop-wizard.js +439 -0
  32. package/lib/public/modules/app-messages.js +1560 -0
  33. package/lib/public/modules/app-misc.js +299 -0
  34. package/lib/public/modules/app-notifications.js +372 -0
  35. package/lib/public/modules/app-panels.js +888 -0
  36. package/lib/public/modules/app-projects.js +798 -0
  37. package/lib/public/modules/app-rate-limit.js +451 -0
  38. package/lib/public/modules/app-rendering.js +597 -0
  39. package/lib/public/modules/app-skills-install.js +234 -0
  40. package/lib/public/modules/command-palette.js +27 -4
  41. package/lib/public/modules/input.js +31 -20
  42. package/lib/public/modules/scheduler-config.js +1532 -0
  43. package/lib/public/modules/scheduler-history.js +79 -0
  44. package/lib/public/modules/scheduler.js +33 -1554
  45. package/lib/public/modules/session-search.js +13 -1
  46. package/lib/public/modules/sidebar-mates.js +812 -0
  47. package/lib/public/modules/sidebar-mobile.js +1269 -0
  48. package/lib/public/modules/sidebar-projects.js +1449 -0
  49. package/lib/public/modules/sidebar-sessions.js +986 -0
  50. package/lib/public/modules/sidebar.js +232 -4591
  51. package/lib/public/modules/store.js +27 -0
  52. package/lib/public/modules/ws-ref.js +7 -0
  53. package/lib/public/style.css +1 -0
  54. package/lib/sdk-bridge.js +96 -717
  55. package/lib/sdk-message-processor.js +587 -0
  56. package/lib/sdk-message-queue.js +42 -0
  57. package/lib/sdk-skill-discovery.js +131 -0
  58. package/lib/server-admin.js +712 -0
  59. package/lib/server-auth.js +737 -0
  60. package/lib/server-dm.js +221 -0
  61. package/lib/server-mates.js +281 -0
  62. package/lib/server-palette.js +110 -0
  63. package/lib/server-settings.js +479 -0
  64. package/lib/server-skills.js +280 -0
  65. package/lib/server.js +246 -2755
  66. package/lib/sessions.js +11 -4
  67. package/lib/users-auth.js +146 -0
  68. package/lib/users-permissions.js +118 -0
  69. package/lib/users-preferences.js +210 -0
  70. package/lib/users.js +48 -398
  71. package/lib/ws-schema.js +498 -0
  72. package/package.json +1 -1
@@ -0,0 +1,212 @@
1
+ // app-favicon.js - Favicon, IO blink, urgent blink, status/activity UI
2
+ // Extracted from app.js (PR-34)
3
+
4
+ import { refreshIcons } from './icons.js';
5
+
6
+ var _ctx = null;
7
+
8
+ // --- Module-owned state ---
9
+ var faviconLink, faviconOrigHref, faviconCanvas, faviconCtx, faviconImg, faviconImgReady;
10
+ var BAND_COLORS = [[0,235,160],[0,200,220],[30,100,255],[88,50,255],[200,60,180],[255,90,50]];
11
+ var faviconAnimTimer = null, faviconAnimFrame = 0;
12
+ var urgentBlinkTimer = null, urgentTitleTimer = null, savedTitle = null;
13
+ var ioTimer = null;
14
+ var sessionIoTimers = {};
15
+ var crossProjectBlinkTimer = null;
16
+
17
+ export function initFavicon(ctx) {
18
+ _ctx = ctx;
19
+
20
+ faviconLink = document.querySelector('link[rel="icon"]');
21
+ faviconCanvas = document.createElement("canvas");
22
+ faviconCanvas.width = 32;
23
+ faviconCanvas.height = 32;
24
+ faviconCtx = faviconCanvas.getContext("2d");
25
+ faviconImg = null;
26
+ faviconImgReady = false;
27
+
28
+ // Load the banded favicon image for masking
29
+ (function () {
30
+ faviconImg = new Image();
31
+ faviconImg.onload = function () { faviconImgReady = true; };
32
+ faviconImg.src = _ctx.basePath + "favicon-banded.png";
33
+ })();
34
+
35
+ // Reset cached favicon href on theme change
36
+ _ctx.onThemeChange(function () { faviconOrigHref = null; });
37
+ }
38
+
39
+ export function updateFavicon(bgColor) {
40
+ if (!faviconLink) return;
41
+ if (!bgColor) {
42
+ if (faviconOrigHref) { faviconLink.href = faviconOrigHref; faviconOrigHref = null; }
43
+ return;
44
+ }
45
+ if (!faviconOrigHref) faviconOrigHref = faviconLink.href;
46
+ // Simple solid-color favicon for non-animated states
47
+ faviconCtx.clearRect(0, 0, 32, 32);
48
+ faviconCtx.fillStyle = bgColor;
49
+ faviconCtx.beginPath();
50
+ faviconCtx.arc(16, 16, 14, 0, Math.PI * 2);
51
+ faviconCtx.fill();
52
+ faviconCtx.fillStyle = "#fff";
53
+ faviconCtx.font = "bold 22px Nunito, sans-serif";
54
+ faviconCtx.textAlign = "center";
55
+ faviconCtx.textBaseline = "middle";
56
+ faviconCtx.fillText("C", 16, 17);
57
+ faviconLink.href = faviconCanvas.toDataURL("image/png");
58
+ }
59
+
60
+ export function drawFaviconAnimFrame() {
61
+ if (!faviconImgReady) return;
62
+ var S = 32;
63
+ var bands = BAND_COLORS.length;
64
+ var totalFrames = bands * 2;
65
+ var offset = faviconAnimFrame % totalFrames;
66
+
67
+ // Draw flowing color bands as background
68
+ faviconCtx.clearRect(0, 0, S, S);
69
+ var bandH = Math.ceil(S / bands);
70
+ for (var i = 0; i < bands + totalFrames; i++) {
71
+ var ci = ((i + offset) % bands + bands) % bands;
72
+ var c = BAND_COLORS[ci];
73
+ faviconCtx.fillStyle = "rgb(" + c[0] + "," + c[1] + "," + c[2] + ")";
74
+ faviconCtx.fillRect(0, (i - offset) * bandH, S, bandH);
75
+ }
76
+
77
+ // Use the banded C image as a mask -- draw it on top with destination-in
78
+ faviconCtx.globalCompositeOperation = "destination-in";
79
+ faviconCtx.drawImage(faviconImg, 0, 0, S, S);
80
+ faviconCtx.globalCompositeOperation = "source-over";
81
+
82
+ faviconLink.href = faviconCanvas.toDataURL("image/png");
83
+ faviconAnimFrame++;
84
+ }
85
+
86
+ export function setSendBtnMode(mode) {
87
+ if (mode === "stop") {
88
+ _ctx.sendBtn.disabled = false;
89
+ _ctx.sendBtn.classList.add("stop");
90
+ _ctx.sendBtn.innerHTML = '<i data-lucide="square"></i>';
91
+ } else {
92
+ _ctx.sendBtn.disabled = false;
93
+ _ctx.sendBtn.classList.remove("stop");
94
+ _ctx.sendBtn.innerHTML = '<i data-lucide="arrow-up"></i>';
95
+ }
96
+ refreshIcons();
97
+ }
98
+
99
+ export function blinkIO() {
100
+ if (!_ctx.connected) return;
101
+ var dot = _ctx.getStatusDot();
102
+ if (dot) dot.classList.add("io");
103
+ // Also blink the active session's processing dot in sidebar (project or mate)
104
+ var sessionDot = document.querySelector(".session-item.active .session-processing") ||
105
+ document.querySelector(".mate-session-item.active .session-processing");
106
+ if (sessionDot) sessionDot.classList.add("io");
107
+ // If active project is a worktree, also blink the parent project dot
108
+ var activeWt = document.querySelector("#icon-strip-projects .icon-strip-wt-item.active");
109
+ var parentDot = null;
110
+ if (activeWt) {
111
+ var group = activeWt.closest(".icon-strip-group");
112
+ if (group) parentDot = group.querySelector(".folder-header .icon-strip-status");
113
+ if (parentDot) parentDot.classList.add("io");
114
+ }
115
+ // Mobile chat chip dot + mobile session dot
116
+ var mobileChipDot = null;
117
+ if (_ctx.dmMode && _ctx.dmTargetUser && _ctx.dmTargetUser.isMate) {
118
+ mobileChipDot = document.querySelector('.mobile-chat-chip[data-mate-id="' + _ctx.dmTargetUser.id + '"] .mobile-chat-chip-dot');
119
+ } else {
120
+ mobileChipDot = document.querySelector('.mobile-chat-chip[data-slug="' + _ctx.currentSlug + '"] .mobile-chat-chip-dot');
121
+ }
122
+ if (mobileChipDot) mobileChipDot.classList.add("io");
123
+ var mobileSessionDot = document.querySelector('.mobile-session-item.active .mobile-session-dot');
124
+ if (mobileSessionDot) mobileSessionDot.classList.add("io");
125
+ clearTimeout(ioTimer);
126
+ ioTimer = setTimeout(function () {
127
+ var d = _ctx.getStatusDot();
128
+ if (d) d.classList.remove("io");
129
+ var sd = document.querySelector(".session-item.active .session-processing.io") ||
130
+ document.querySelector(".mate-session-item.active .session-processing.io");
131
+ if (sd) sd.classList.remove("io");
132
+ if (parentDot) parentDot.classList.remove("io");
133
+ if (mobileChipDot) mobileChipDot.classList.remove("io");
134
+ if (mobileSessionDot) mobileSessionDot.classList.remove("io");
135
+ }, 80);
136
+ }
137
+
138
+ export function blinkSessionDot(sessionId) {
139
+ var el = document.querySelector('.session-item[data-session-id="' + sessionId + '"] .session-processing');
140
+ if (!el) return;
141
+ el.classList.add("io");
142
+ clearTimeout(sessionIoTimers[sessionId]);
143
+ sessionIoTimers[sessionId] = setTimeout(function () {
144
+ el.classList.remove("io");
145
+ delete sessionIoTimers[sessionId];
146
+ }, 80);
147
+ }
148
+
149
+ export function updateCrossProjectBlink() {
150
+ if (crossProjectBlinkTimer) { clearTimeout(crossProjectBlinkTimer); crossProjectBlinkTimer = null; }
151
+ function doBlink() {
152
+ var dots = document.querySelectorAll("#icon-strip-projects .icon-strip-item:not(.active) .icon-strip-status.processing, #icon-strip-projects .icon-strip-wt-item:not(.active) .icon-strip-status.processing, #icon-strip-users .icon-strip-mate:not(.active) .icon-strip-status.processing");
153
+ // Also blink mobile chat chip dots (same icon-strip-status class inside chips)
154
+ var mobileDots = document.querySelectorAll(".mobile-chat-chip .icon-strip-status.processing");
155
+ var allDots = [];
156
+ for (var i = 0; i < dots.length; i++) allDots.push(dots[i]);
157
+ for (var m = 0; m < mobileDots.length; m++) allDots.push(mobileDots[m]);
158
+ if (allDots.length === 0) { crossProjectBlinkTimer = null; return; }
159
+ for (var i2 = 0; i2 < allDots.length; i2++) { allDots[i2].classList.add("io"); }
160
+ setTimeout(function () {
161
+ for (var j = 0; j < allDots.length; j++) { allDots[j].classList.remove("io"); }
162
+ crossProjectBlinkTimer = setTimeout(doBlink, 150 + Math.random() * 350);
163
+ }, 80);
164
+ }
165
+ crossProjectBlinkTimer = setTimeout(doBlink, 50);
166
+ }
167
+
168
+ export function startUrgentBlink() {
169
+ if (urgentBlinkTimer) return;
170
+ savedTitle = document.title;
171
+ if (!faviconOrigHref && faviconLink) faviconOrigHref = faviconLink.href;
172
+ faviconAnimFrame = 0;
173
+ // Color flow animation at ~12fps
174
+ urgentBlinkTimer = setInterval(drawFaviconAnimFrame, 83);
175
+ // Title blink separately
176
+ var titleTick = 0;
177
+ urgentTitleTimer = setInterval(function () {
178
+ document.title = titleTick % 2 === 0 ? "\u26A0 Input needed" : savedTitle;
179
+ titleTick++;
180
+ }, 500);
181
+ }
182
+
183
+ export function stopUrgentBlink() {
184
+ if (!urgentBlinkTimer) return;
185
+ clearInterval(urgentBlinkTimer);
186
+ clearInterval(urgentTitleTimer);
187
+ urgentBlinkTimer = null;
188
+ urgentTitleTimer = null;
189
+ faviconAnimFrame = 0;
190
+ updateFavicon(null);
191
+ if (savedTitle) document.title = savedTitle;
192
+ savedTitle = null;
193
+ }
194
+
195
+ export function setActivity(text) {
196
+ if (text) {
197
+ if (!_ctx.getActivityEl()) {
198
+ var _actEl = document.createElement("div");
199
+ _actEl.className = "activity-inline";
200
+ _actEl.innerHTML =
201
+ '<div class="mate-thinking-dots"><span></span><span></span><span></span></div>';
202
+ _ctx.setActivityEl(_actEl);
203
+ _ctx.addToMessages(_actEl);
204
+ }
205
+ _ctx.scrollToBottom();
206
+ } else {
207
+ if (_ctx.getActivityEl()) {
208
+ _ctx.getActivityEl().remove();
209
+ _ctx.setActivityEl(null);
210
+ }
211
+ }
212
+ }
@@ -0,0 +1,229 @@
1
+ // app-header.js - Session rename, session info popover, progressive history loading
2
+ // Extracted from app.js (PR-34)
3
+
4
+ import { refreshIcons, iconHtml } from './icons.js';
5
+ import { escapeHtml, copyToClipboard } from './utils.js';
6
+
7
+ var _ctx = null;
8
+
9
+ // --- Module-owned state ---
10
+ var sessionInfoPopover = null;
11
+ var historySentinelObserver = null;
12
+
13
+ export function initHeader(ctx) {
14
+ _ctx = ctx;
15
+
16
+ // --- Header session rename ---
17
+ if (_ctx.headerRenameBtn) {
18
+ _ctx.headerRenameBtn.addEventListener("click", function () {
19
+ if (!_ctx.activeSessionId) return;
20
+ var currentText = _ctx.headerTitleEl.textContent;
21
+ var input = document.createElement("input");
22
+ input.type = "text";
23
+ input.className = "header-rename-input";
24
+ input.value = currentText;
25
+ _ctx.headerTitleEl.style.display = "none";
26
+ _ctx.headerRenameBtn.style.display = "none";
27
+ _ctx.headerTitleEl.parentNode.insertBefore(input, _ctx.headerTitleEl.nextSibling);
28
+ input.focus();
29
+ input.select();
30
+
31
+ function commit() {
32
+ var newTitle = input.value.trim();
33
+ var ws = _ctx.getWs();
34
+ if (newTitle && newTitle !== currentText && ws && ws.readyState === 1) {
35
+ ws.send(JSON.stringify({ type: "rename_session", id: _ctx.activeSessionId, title: newTitle }));
36
+ _ctx.headerTitleEl.textContent = newTitle;
37
+ }
38
+ input.remove();
39
+ _ctx.headerTitleEl.style.display = "";
40
+ _ctx.headerRenameBtn.style.display = "";
41
+ }
42
+
43
+ input.addEventListener("keydown", function (e) {
44
+ if (e.key === "Enter") { e.preventDefault(); commit(); }
45
+ if (e.key === "Escape") {
46
+ e.preventDefault();
47
+ input.remove();
48
+ _ctx.headerTitleEl.style.display = "";
49
+ _ctx.headerRenameBtn.style.display = "";
50
+ }
51
+ });
52
+ input.addEventListener("blur", commit);
53
+ });
54
+ }
55
+
56
+ // --- Session info popover ---
57
+ if (_ctx.headerInfoBtn) {
58
+ _ctx.headerInfoBtn.addEventListener("click", function (e) {
59
+ e.stopPropagation();
60
+ if (sessionInfoPopover) { closeSessionInfoPopover(); return; }
61
+
62
+ var pop = document.createElement("div");
63
+ pop.className = "session-info-popover";
64
+
65
+ function addRow(label, value) {
66
+ var val = value == null ? "-" : String(value);
67
+ var row = document.createElement("div");
68
+ row.className = "info-row";
69
+ row.innerHTML =
70
+ '<span class="info-label">' + label + '</span>' +
71
+ '<span class="info-value">' + escapeHtml(val) + '</span>' +
72
+ '<button class="info-copy-btn" title="Copy">' + iconHtml("copy") + '</button>';
73
+ var btn = row.querySelector(".info-copy-btn");
74
+ btn.addEventListener("click", function () {
75
+ copyToClipboard(value || "").then(function () {
76
+ btn.innerHTML = iconHtml("check");
77
+ refreshIcons();
78
+ setTimeout(function () { btn.innerHTML = iconHtml("copy"); refreshIcons(); }, 1200);
79
+ });
80
+ });
81
+ pop.appendChild(row);
82
+ }
83
+
84
+ if (_ctx.cliSessionId) addRow("Session ID", _ctx.cliSessionId);
85
+ if (_ctx.activeSessionId) addRow("Local ID", _ctx.activeSessionId);
86
+ if (_ctx.cliSessionId) addRow("Resume", "claude --resume " + _ctx.cliSessionId);
87
+
88
+ document.body.appendChild(pop);
89
+ sessionInfoPopover = pop;
90
+ refreshIcons();
91
+
92
+ var btnRect = _ctx.headerInfoBtn.getBoundingClientRect();
93
+ pop.style.top = (btnRect.bottom + 6) + "px";
94
+ pop.style.left = btnRect.left + "px";
95
+ var popRect = pop.getBoundingClientRect();
96
+ if (popRect.right > window.innerWidth - 8) {
97
+ pop.style.left = (window.innerWidth - popRect.width - 8) + "px";
98
+ }
99
+ });
100
+
101
+ document.addEventListener("click", function (e) {
102
+ if (sessionInfoPopover && !sessionInfoPopover.contains(e.target) && !e.target.closest("#header-info-btn")) {
103
+ closeSessionInfoPopover();
104
+ }
105
+ });
106
+ }
107
+ }
108
+
109
+ export function closeSessionInfoPopover() {
110
+ if (sessionInfoPopover) {
111
+ sessionInfoPopover.remove();
112
+ sessionInfoPopover = null;
113
+ }
114
+ }
115
+
116
+ export function updateHistorySentinel() {
117
+ var existing = _ctx.messagesEl.querySelector(".history-sentinel");
118
+ if (_ctx.historyFrom > 0) {
119
+ if (!existing) {
120
+ var sentinel = document.createElement("div");
121
+ sentinel.className = "history-sentinel";
122
+ sentinel.innerHTML = '<button class="load-more-btn">Load earlier messages</button>';
123
+ sentinel.querySelector(".load-more-btn").addEventListener("click", function () {
124
+ requestMoreHistory();
125
+ });
126
+ _ctx.messagesEl.insertBefore(sentinel, _ctx.messagesEl.firstChild);
127
+
128
+ // Auto-load when sentinel scrolls into view
129
+ if (historySentinelObserver) historySentinelObserver.disconnect();
130
+ historySentinelObserver = new IntersectionObserver(function (entries) {
131
+ if (entries[0].isIntersecting && !_ctx.loadingMore && _ctx.historyFrom > 0) {
132
+ requestMoreHistory();
133
+ }
134
+ }, { root: _ctx.messagesEl, rootMargin: "200px 0px 0px 0px" });
135
+ historySentinelObserver.observe(sentinel);
136
+ }
137
+ } else {
138
+ if (existing) existing.remove();
139
+ if (historySentinelObserver) { historySentinelObserver.disconnect(); historySentinelObserver = null; }
140
+ }
141
+ }
142
+
143
+ export function requestMoreHistory() {
144
+ var ws = _ctx.getWs();
145
+ if (_ctx.loadingMore || _ctx.historyFrom <= 0 || !ws || !_ctx.connected) return;
146
+ _ctx.loadingMore = true;
147
+ var btn = _ctx.messagesEl.querySelector(".load-more-btn");
148
+ if (btn) btn.classList.add("loading");
149
+ ws.send(JSON.stringify({ type: "load_more_history", before: _ctx.historyFrom }));
150
+ }
151
+
152
+ export function prependOlderHistory(items, meta) {
153
+ // Save current rendering state
154
+ var savedMsgEl = _ctx.getCurrentMsgEl();
155
+ var savedActivity = _ctx.getActivityEl();
156
+ var savedFullText = _ctx.getCurrentFullText();
157
+ var savedTurnCounter = _ctx.getTurnCounter();
158
+ var savedToolsState = _ctx.saveToolState();
159
+ // Save context & usage so old result messages don't overwrite current values
160
+ var savedContext = JSON.parse(JSON.stringify(_ctx.getContextData()));
161
+ var savedUsage = JSON.parse(JSON.stringify(_ctx.getSessionUsage()));
162
+
163
+ // Reset to initial values for clean rendering
164
+ _ctx.setCurrentMsgEl(null);
165
+ _ctx.setActivityEl(null);
166
+ _ctx.setCurrentFullText("");
167
+ _ctx.setTurnCounter(0);
168
+ _ctx.resetToolState();
169
+
170
+ // Set prepend anchor to insert before existing content
171
+ // Skip the sentinel itself when setting anchor
172
+ var firstReal = _ctx.messagesEl.querySelector(".history-sentinel");
173
+ _ctx.setPrependAnchor(firstReal ? firstReal.nextSibling : _ctx.messagesEl.firstChild);
174
+
175
+ // Remember the first existing content element and its position
176
+ var anchorEl = _ctx.getPrependAnchor();
177
+ var anchorOffset = anchorEl ? anchorEl.getBoundingClientRect().top : 0;
178
+
179
+ // Process each item through the rendering pipeline
180
+ for (var i = 0; i < items.length; i++) {
181
+ _ctx.processMessage(items[i]);
182
+ }
183
+
184
+ // Finalize any open assistant block from the batch
185
+ _ctx.finalizeAssistantBlock();
186
+
187
+ // Clear prepend mode
188
+ _ctx.setPrependAnchor(null);
189
+
190
+ // Restore saved state
191
+ _ctx.setCurrentMsgEl(savedMsgEl);
192
+ _ctx.setActivityEl(savedActivity);
193
+ _ctx.setCurrentFullText(savedFullText);
194
+ _ctx.setTurnCounter(savedTurnCounter);
195
+ _ctx.restoreToolState(savedToolsState);
196
+ // Restore context & usage (old result messages must not overwrite current values)
197
+ _ctx.setContextData(savedContext);
198
+ _ctx.setSessionUsage(savedUsage);
199
+ _ctx.updateContextPanel();
200
+ _ctx.updateUsagePanel();
201
+
202
+ // Fix scroll: restore anchor element to same visual position
203
+ if (anchorEl) {
204
+ var newTop = anchorEl.getBoundingClientRect().top;
205
+ _ctx.messagesEl.scrollTop += (newTop - anchorOffset);
206
+ }
207
+
208
+ // Update state
209
+ _ctx.historyFrom = meta.from;
210
+ _ctx.loadingMore = false;
211
+
212
+ // Renumber data-turn attributes in DOM order
213
+ var turnEls = _ctx.messagesEl.querySelectorAll("[data-turn]");
214
+ for (var t = 0; t < turnEls.length; t++) {
215
+ turnEls[t].dataset.turn = t + 1;
216
+ }
217
+ _ctx.setTurnCounter(turnEls.length);
218
+
219
+ // Update sentinel
220
+ if (meta.hasMore) {
221
+ var btn = _ctx.messagesEl.querySelector(".load-more-btn");
222
+ if (btn) btn.classList.remove("loading");
223
+ } else {
224
+ updateHistorySentinel();
225
+ }
226
+
227
+ // Notify in-session search that history was prepended (for pending scroll targets)
228
+ _ctx.onSessionSearchHistoryPrepended();
229
+ }