Keep the transcript pinned to the bottom as its content changes
This commit is contained in:
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+22
-13
@@ -112,6 +112,20 @@ export function mountChat(root, src, makeHead) {
|
||||
const head = h("header", { class: "task-header" });
|
||||
const transcript = h("div", { class: "transcript" });
|
||||
const composer = h("div", { class: "composer" });
|
||||
let stuck = true;
|
||||
const pin = () => {
|
||||
if (stuck) {
|
||||
transcript.scrollTop = transcript.scrollHeight;
|
||||
}
|
||||
};
|
||||
transcript.addEventListener("scroll", () => {
|
||||
stuck = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
|
||||
});
|
||||
transcript.addEventListener("load", pin, true);
|
||||
const resizes = new ResizeObserver(pin);
|
||||
resizes.observe(transcript);
|
||||
const mutations = new MutationObserver(pin);
|
||||
mutations.observe(transcript, { childList: true, subtree: true, characterData: true });
|
||||
const visibleReports = new Map();
|
||||
const acked = new Set();
|
||||
const ackVisible = () => {
|
||||
@@ -171,11 +185,8 @@ export function mountChat(root, src, makeHead) {
|
||||
if (running === !spinner.hidden) {
|
||||
return;
|
||||
}
|
||||
const pinned = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
|
||||
spinner.hidden = !running;
|
||||
if (pinned) {
|
||||
transcript.scrollTop = transcript.scrollHeight;
|
||||
}
|
||||
pin();
|
||||
};
|
||||
const putBefore = (el, anchor) => {
|
||||
if (anchor?.parentNode === transcript) {
|
||||
@@ -235,7 +246,8 @@ export function mountChat(root, src, makeHead) {
|
||||
pending.push(entry);
|
||||
ctx.run = undefined;
|
||||
putBefore(el, spinner);
|
||||
transcript.scrollTop = transcript.scrollHeight;
|
||||
stuck = true;
|
||||
pin();
|
||||
return () => {
|
||||
el.remove();
|
||||
pending = pending.filter((p) => p !== entry);
|
||||
@@ -299,12 +311,9 @@ export function mountChat(root, src, makeHead) {
|
||||
return;
|
||||
}
|
||||
stream.timer = 0;
|
||||
const pinned = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
|
||||
stream.el.hidden = quietReply.startsWith(stream.text.trim());
|
||||
replace(stream.el, markdown(stream.text, "text md", { taskId: ctx.taskId }));
|
||||
if (pinned) {
|
||||
transcript.scrollTop = transcript.scrollHeight;
|
||||
}
|
||||
pin();
|
||||
}, 100);
|
||||
};
|
||||
const openHandoff = () => {
|
||||
@@ -328,7 +337,6 @@ export function mountChat(root, src, makeHead) {
|
||||
return handoff.summary;
|
||||
};
|
||||
const appendEvents = (events) => {
|
||||
const pinned = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
|
||||
let contextChanged = false;
|
||||
headImpl.onEvents?.(events);
|
||||
for (const ev of events) {
|
||||
@@ -407,11 +415,10 @@ export function mountChat(root, src, makeHead) {
|
||||
renderComposer();
|
||||
}
|
||||
}
|
||||
if (pinned) {
|
||||
transcript.scrollTop = transcript.scrollHeight;
|
||||
}
|
||||
pin();
|
||||
};
|
||||
const renderAll = () => {
|
||||
stuck = true;
|
||||
observer.disconnect();
|
||||
visibleReports.clear();
|
||||
dropStream();
|
||||
@@ -450,6 +457,8 @@ export function mountChat(root, src, makeHead) {
|
||||
document.removeEventListener("visibilitychange", ackVisible);
|
||||
headImpl.dispose?.();
|
||||
observer.disconnect();
|
||||
resizes.disconnect();
|
||||
mutations.disconnect();
|
||||
dropStream();
|
||||
};
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+2
-1
@@ -8,7 +8,8 @@ import { openSwitcher } from "./navbar.js";
|
||||
import { findTask, focusedProject, focusTab, on, onEach, state, tabs } from "./state.js";
|
||||
import { mountPreview } from "./preview.js";
|
||||
import { mountProject } from "./project.js";
|
||||
import { mountTranscript } from "./task.js";import { mountTerminal } from "./terminal.js";
|
||||
import { mountTranscript } from "./task.js";
|
||||
import { mountTerminal } from "./terminal.js";
|
||||
|
||||
const views: Record<TaskTab, { label: string; mount: (root: HTMLElement) => () => void }> = {
|
||||
transcript: { label: "Transcript", mount: mountTranscript },
|
||||
|
||||
+23
-13
@@ -176,6 +176,21 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
|
||||
const transcript = h("div", { class: "transcript" });
|
||||
const composer = h("div", { class: "composer" });
|
||||
|
||||
let stuck = true;
|
||||
const pin = () => {
|
||||
if (stuck) {
|
||||
transcript.scrollTop = transcript.scrollHeight;
|
||||
}
|
||||
};
|
||||
transcript.addEventListener("scroll", () => {
|
||||
stuck = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
|
||||
});
|
||||
transcript.addEventListener("load", pin, true);
|
||||
const resizes = new ResizeObserver(pin);
|
||||
resizes.observe(transcript);
|
||||
const mutations = new MutationObserver(pin);
|
||||
mutations.observe(transcript, { childList: true, subtree: true, characterData: true });
|
||||
|
||||
const visibleReports = new Map<Element, string>();
|
||||
const acked = new Set<string>();
|
||||
const ackVisible = () => {
|
||||
@@ -239,11 +254,8 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
|
||||
if (running === !spinner.hidden) {
|
||||
return;
|
||||
}
|
||||
const pinned = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
|
||||
spinner.hidden = !running;
|
||||
if (pinned) {
|
||||
transcript.scrollTop = transcript.scrollHeight;
|
||||
}
|
||||
pin();
|
||||
};
|
||||
const putBefore = (el: HTMLElement, anchor: HTMLElement | undefined) => {
|
||||
if (anchor?.parentNode === transcript) {
|
||||
@@ -309,7 +321,8 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
|
||||
pending.push(entry);
|
||||
ctx.run = undefined;
|
||||
putBefore(el, spinner);
|
||||
transcript.scrollTop = transcript.scrollHeight;
|
||||
stuck = true;
|
||||
pin();
|
||||
return () => {
|
||||
el.remove();
|
||||
pending = pending.filter((p) => p !== entry);
|
||||
@@ -376,12 +389,9 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
|
||||
return;
|
||||
}
|
||||
stream.timer = 0;
|
||||
const pinned = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
|
||||
stream.el.hidden = quietReply.startsWith(stream.text.trim());
|
||||
replace(stream.el, markdown(stream.text, "text md", { taskId: ctx.taskId }));
|
||||
if (pinned) {
|
||||
transcript.scrollTop = transcript.scrollHeight;
|
||||
}
|
||||
pin();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
@@ -406,7 +416,6 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
|
||||
};
|
||||
|
||||
const appendEvents = (events: ByreEvent[]) => {
|
||||
const pinned = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
|
||||
let contextChanged = false;
|
||||
headImpl.onEvents?.(events);
|
||||
for (const ev of events) {
|
||||
@@ -485,12 +494,11 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
|
||||
renderComposer();
|
||||
}
|
||||
}
|
||||
if (pinned) {
|
||||
transcript.scrollTop = transcript.scrollHeight;
|
||||
}
|
||||
pin();
|
||||
};
|
||||
|
||||
const renderAll = () => {
|
||||
stuck = true;
|
||||
observer.disconnect();
|
||||
visibleReports.clear();
|
||||
dropStream();
|
||||
@@ -531,6 +539,8 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
|
||||
document.removeEventListener("visibilitychange", ackVisible);
|
||||
headImpl.dispose?.();
|
||||
observer.disconnect();
|
||||
resizes.disconnect();
|
||||
mutations.disconnect();
|
||||
dropStream();
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user