Load the lead's scrollback 50 turns at a time
This commit is contained in:
Vendored
+73
-6
@@ -210,11 +210,17 @@ export function mountChat(root, src, makeHead) {
|
||||
const headImpl = makeHead(() => renderHead());
|
||||
const scanner = h("div", { class: "scanner", hidden: true });
|
||||
const loading = h("div", { class: "empty" }, "loading…");
|
||||
const earlier = h("div");
|
||||
let sink;
|
||||
const setRunning = (running) => {
|
||||
transcript.classList.toggle("running", running);
|
||||
scanner.hidden = !running;
|
||||
};
|
||||
const putBefore = (el, anchor) => {
|
||||
if (sink) {
|
||||
sink.append(el);
|
||||
return;
|
||||
}
|
||||
if (anchor?.parentNode === transcript) {
|
||||
transcript.insertBefore(el, anchor);
|
||||
}
|
||||
@@ -394,9 +400,8 @@ export function mountChat(root, src, makeHead) {
|
||||
}
|
||||
return handoff.summary;
|
||||
};
|
||||
const appendEvents = (events) => {
|
||||
const renderEvents = (events) => {
|
||||
let contextChanged = false;
|
||||
headImpl.onEvents?.(events);
|
||||
for (const ev of events) {
|
||||
const p = ev.payload;
|
||||
const parent = parentOf(p);
|
||||
@@ -411,12 +416,15 @@ export function mountChat(root, src, makeHead) {
|
||||
claudeTaskStatus(p, ctx);
|
||||
continue;
|
||||
}
|
||||
if ((ev.kind === "agent.message" && !parent) || ev.kind === "turn.ended" || ev.kind === "user.message") {
|
||||
if (!sink && ((ev.kind === "agent.message" && !parent) || ev.kind === "turn.ended" || ev.kind === "user.message")) {
|
||||
dropStream();
|
||||
}
|
||||
if (ev.kind === "user.message" && ev.actor !== "byre") {
|
||||
if (!sink && ev.kind === "user.message" && ev.actor !== "byre") {
|
||||
confirmPending(String(p["text"] ?? ""));
|
||||
}
|
||||
if (ev.kind === "session.context" && sink) {
|
||||
continue;
|
||||
}
|
||||
if (ev.kind === "session.context" && ev.agent_session_id) {
|
||||
const prev = context.get(ev.agent_session_id);
|
||||
const window = Number(p["window"]) || prev?.window || 0;
|
||||
@@ -501,6 +509,11 @@ export function mountChat(root, src, makeHead) {
|
||||
}
|
||||
put(el);
|
||||
}
|
||||
return contextChanged;
|
||||
};
|
||||
const appendEvents = (events) => {
|
||||
headImpl.onEvents?.(events);
|
||||
const contextChanged = renderEvents(events);
|
||||
syncRunning();
|
||||
if (contextChanged) {
|
||||
renderHead();
|
||||
@@ -513,6 +526,54 @@ export function mountChat(root, src, makeHead) {
|
||||
pin();
|
||||
jump();
|
||||
};
|
||||
const prependEvents = (events) => {
|
||||
const live = ctx;
|
||||
const fromBottom = transcript.scrollHeight - transcript.scrollTop;
|
||||
const box = document.createDocumentFragment();
|
||||
ctx = newCtx();
|
||||
sink = box;
|
||||
renderEvents(events);
|
||||
sink = undefined;
|
||||
ctx = live;
|
||||
syncRunning();
|
||||
fillTodoLinks(box, models[0]);
|
||||
earlier.after(box);
|
||||
transcript.scrollTop = transcript.scrollHeight - fromBottom;
|
||||
};
|
||||
let disposed = false;
|
||||
let fetching = false;
|
||||
const loadOlder = async () => {
|
||||
const older = src.older;
|
||||
if (!older || fetching || !older.has()) {
|
||||
return;
|
||||
}
|
||||
fetching = true;
|
||||
replace(earlier, h("div", { class: "empty" }, "loading earlier…"));
|
||||
try {
|
||||
const events = await older.load();
|
||||
replace(earlier);
|
||||
if (!disposed) {
|
||||
prependEvents(events);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
fetching = false;
|
||||
replace(earlier);
|
||||
}
|
||||
watchTop();
|
||||
};
|
||||
const top = new IntersectionObserver((entries) => {
|
||||
if (entries.some((e) => e.isIntersecting)) {
|
||||
loadOlder().catch((err) => console.error("load earlier events", err));
|
||||
}
|
||||
}, { root: transcript, rootMargin: "400px 0px 0px 0px" });
|
||||
const watchTop = () => {
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
top.unobserve(earlier);
|
||||
top.observe(earlier);
|
||||
};
|
||||
const renderAll = () => {
|
||||
stuck = true;
|
||||
dropStream();
|
||||
@@ -529,12 +590,13 @@ export function mountChat(root, src, makeHead) {
|
||||
replace(transcript, h("div", { class: "empty" }, src.empty), scanner);
|
||||
return;
|
||||
}
|
||||
replace(transcript, scanner);
|
||||
replace(transcript, earlier, scanner);
|
||||
if (src.loading()) {
|
||||
transcript.prepend(loading);
|
||||
}
|
||||
appendEvents(src.events());
|
||||
renderComposer();
|
||||
watchTop();
|
||||
};
|
||||
const offs = [
|
||||
onEach(src.resetTopics, renderAll),
|
||||
@@ -544,7 +606,10 @@ export function mountChat(root, src, makeHead) {
|
||||
}),
|
||||
onEach([src.transcriptTopic], (fresh) => appendEvents(fresh)),
|
||||
onEach([src.streamTopic], (ev) => appendDelta(ev)),
|
||||
onEach([src.readyTopic], () => loading.remove()),
|
||||
onEach([src.readyTopic], () => {
|
||||
loading.remove();
|
||||
watchTop();
|
||||
}),
|
||||
onEach(["tasks", "sessions"], () => fillTodoLinks(transcript, models[0])),
|
||||
on("jump", jump),
|
||||
];
|
||||
@@ -554,6 +619,8 @@ export function mountChat(root, src, makeHead) {
|
||||
for (const off of offs) {
|
||||
off();
|
||||
}
|
||||
disposed = true;
|
||||
top.disconnect();
|
||||
headImpl.dispose?.();
|
||||
resizes.disconnect();
|
||||
mutations.disconnect();
|
||||
|
||||
Reference in New Issue
Block a user