clay-server 2.27.0-beta.9 → 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.
- package/README.md +10 -0
- package/lib/daemon-projects.js +164 -0
- package/lib/daemon.js +13 -126
- package/lib/mates-identity.js +132 -0
- package/lib/mates-knowledge.js +113 -0
- package/lib/mates-prompts.js +398 -0
- package/lib/mates.js +40 -599
- package/lib/project-connection.js +2 -0
- package/lib/project-http.js +4 -2
- package/lib/project-loop.js +110 -48
- package/lib/project-mate-interaction.js +4 -0
- package/lib/project-notifications.js +210 -0
- package/lib/project-sessions.js +5 -2
- package/lib/project-user-message.js +2 -1
- package/lib/project.js +26 -2
- package/lib/public/app.js +1193 -8517
- package/lib/public/css/command-palette.css +14 -0
- package/lib/public/css/loop.css +301 -0
- package/lib/public/css/notifications-center.css +190 -0
- package/lib/public/css/rewind.css +6 -0
- package/lib/public/index.html +89 -35
- package/lib/public/modules/app-connection.js +160 -0
- package/lib/public/modules/app-cursors.js +473 -0
- package/lib/public/modules/app-debate-ui.js +389 -0
- package/lib/public/modules/app-dm.js +627 -0
- package/lib/public/modules/app-favicon.js +212 -0
- package/lib/public/modules/app-header.js +229 -0
- package/lib/public/modules/app-home-hub.js +600 -0
- package/lib/public/modules/app-loop-ui.js +589 -0
- package/lib/public/modules/app-loop-wizard.js +439 -0
- package/lib/public/modules/app-messages.js +1560 -0
- package/lib/public/modules/app-misc.js +299 -0
- package/lib/public/modules/app-notifications.js +372 -0
- package/lib/public/modules/app-panels.js +888 -0
- package/lib/public/modules/app-projects.js +798 -0
- package/lib/public/modules/app-rate-limit.js +451 -0
- package/lib/public/modules/app-rendering.js +597 -0
- package/lib/public/modules/app-skills-install.js +234 -0
- package/lib/public/modules/command-palette.js +27 -4
- package/lib/public/modules/input.js +31 -20
- package/lib/public/modules/scheduler-config.js +1532 -0
- package/lib/public/modules/scheduler-history.js +79 -0
- package/lib/public/modules/scheduler.js +33 -1554
- package/lib/public/modules/session-search.js +13 -1
- package/lib/public/modules/sidebar-mates.js +812 -0
- package/lib/public/modules/sidebar-mobile.js +1269 -0
- package/lib/public/modules/sidebar-projects.js +1449 -0
- package/lib/public/modules/sidebar-sessions.js +986 -0
- package/lib/public/modules/sidebar.js +232 -4591
- package/lib/public/modules/store.js +27 -0
- package/lib/public/modules/ws-ref.js +7 -0
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +96 -717
- package/lib/sdk-message-processor.js +587 -0
- package/lib/sdk-message-queue.js +42 -0
- package/lib/sdk-skill-discovery.js +131 -0
- package/lib/server-admin.js +712 -0
- package/lib/server-auth.js +737 -0
- package/lib/server-dm.js +221 -0
- package/lib/server-mates.js +281 -0
- package/lib/server-palette.js +110 -0
- package/lib/server-settings.js +479 -0
- package/lib/server-skills.js +280 -0
- package/lib/server.js +246 -2755
- package/lib/sessions.js +11 -4
- package/lib/users-auth.js +146 -0
- package/lib/users-permissions.js +118 -0
- package/lib/users-preferences.js +210 -0
- package/lib/users.js +48 -398
- package/lib/ws-schema.js +498 -0
- 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
|
+
}
|