Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]
/** Distance (px) from the bottom within which we consider the user "near bottom". */
const NEAR_BOTTOM_THRESHOLD = 450;
type ScrollHost = {
updateComplete: Promise<unknown>;
querySelector: (selectors: string) => Element | null;
style: CSSStyleDeclaration;
chatScrollFrame: number | null;
chatScrollTimeout: number | null;
chatHasAutoScrolled: boolean;
chatUserNearBottom: boolean;
chatNewMessagesBelow: boolean;
logsScrollFrame: number | null;
logsAtBottom: boolean;
topbarObserver: ResizeObserver | null;
};
function queryHost(host: Partial<ScrollHost>, selectors: string): Element | null {
return typeof host.querySelector === "function" ? host.querySelector(selectors) : null;
}
export function scheduleChatScroll(host: ScrollHost, force = false, smooth = false) {
if (host.chatScrollFrame) {
cancelAnimationFrame(host.chatScrollFrame);
}
if (host.chatScrollTimeout != null) {
clearTimeout(host.chatScrollTimeout);
host.chatScrollTimeout = null;
}
const pickScrollTarget = () => {
const container = queryHost(host, ".chat-thread") as HTMLElement | null;
if (container) {
const overflowY = getComputedStyle(container).overflowY;
const canScroll =
overflowY === "auto" ||
overflowY === "scroll" ||
container.scrollHeight - container.clientHeight > 1;
if (canScroll) {
return container;
}
}
return (document.scrollingElement ?? document.documentElement) as HTMLElement | null;
};
// Wait for Lit render to complete, then scroll
void host.updateComplete.then(() => {
host.chatScrollFrame = requestAnimationFrame(() => {
host.chatScrollFrame = null;
const target = pickScrollTarget();
if (!target) {
return;
}
const distanceFromBottom = target.scrollHeight - target.scrollTop - target.clientHeight;
// force=true only overrides when we haven't auto-scrolled yet (initial load).
// After initial load, respect the user's scroll position.
const effectiveForce = force && !host.chatHasAutoScrolled;
const shouldStick =
effectiveForce || host.chatUserNearBottom || distanceFromBottom < NEAR_BOTTOM_THRESHOLD;
if (!shouldStick) {
// User is scrolled up — flag that new content arrived below.
host.chatNewMessagesBelow = true;
return;
}
if (effectiveForce) {
host.chatHasAutoScrolled = true;
}
const smoothEnabled =
smooth &&
(typeof window === "undefined" ||
typeof window.matchMedia !== "function" ||
!window.matchMedia("(prefers-reduced-motion: reduce)").matches);
const scrollTop = target.scrollHeight;
if (typeof target.scrollTo === "function") {
target.scrollTo({ top: scrollTop, behavior: smoothEnabled ? "smooth" : "auto" });
} else {
target.scrollTop = scrollTop;
}
host.chatUserNearBottom = true;
host.chatNewMessagesBelow = false;
const retryDelay = effectiveForce ? 150 : 120;
host.chatScrollTimeout = window.setTimeout(() => {
host.chatScrollTimeout = null;
const latest = pickScrollTarget();
if (!latest) {
return;
}
const latestDistanceFromBottom =
latest.scrollHeight - latest.scrollTop - latest.clientHeight;
const shouldStickRetry =
effectiveForce ||
host.chatUserNearBottom ||
latestDistanceFromBottom < NEAR_BOTTOM_THRESHOLD;
if (!shouldStickRetry) {
return;
}
latest.scrollTop = latest.scrollHeight;
host.chatUserNearBottom = true;
}, retryDelay);
});
});
}
export function scheduleLogsScroll(host: ScrollHost, force = false) {
if (host.logsScrollFrame) {
cancelAnimationFrame(host.logsScrollFrame);
}
void host.updateComplete.then(() => {
host.logsScrollFrame = requestAnimationFrame(() => {
host.logsScrollFrame = null;
const container = queryHost(host, ".log-stream") as HTMLElement | null;
if (!container) {
return;
}
const distanceFromBottom =
container.scrollHeight - container.scrollTop - container.clientHeight;
const shouldStick = force || distanceFromBottom < 80;
if (!shouldStick) {
return;
}
container.scrollTop = container.scrollHeight;
});
});
}
export function handleChatScroll(host: ScrollHost, event: Event) {
const container = event.currentTarget as HTMLElement | null;
if (!container) {
return;
}
const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
host.chatUserNearBottom = distanceFromBottom < NEAR_BOTTOM_THRESHOLD;
// Clear the "new messages below" indicator when user scrolls back to bottom.
if (host.chatUserNearBottom) {
host.chatNewMessagesBelow = false;
}
}
export function handleLogsScroll(host: ScrollHost, event: Event) {
const container = event.currentTarget as HTMLElement | null;
if (!container) {
return;
}
const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
host.logsAtBottom = distanceFromBottom < 80;
}
export function resetChatScroll(host: ScrollHost) {
host.chatHasAutoScrolled = false;
host.chatUserNearBottom = true;
host.chatNewMessagesBelow = false;
}
export function exportLogs(lines: string[], label: string) {
if (lines.length === 0) {
return;
}
const blob = new Blob([`${lines.join("\n")}\n`], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
const stamp = new Date().toISOString().slice(0, 19).replace(/[:T]/g, "-");
anchor.href = url;
anchor.download = `openclaw-logs-${label}-${stamp}.log`;
anchor.click();
URL.revokeObjectURL(url);
}
export function observeTopbar(host: ScrollHost) {
if (typeof ResizeObserver === "undefined") {
return;
}
const topbar = queryHost(host, ".topbar");
if (!topbar) {
return;
}
const update = () => {
const { height } = topbar.getBoundingClientRect();
host.style.setProperty("--topbar-height", `${height}px`);
};
update();
host.topbarObserver = new ResizeObserver(() => update());
host.topbarObserver.observe(topbar);
}
¤ Dauer der Verarbeitung: 0.25 Sekunden
(vorverarbeitet am 2026-04-27)
¤
*© Formatika GbR, Deutschland