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 head = h("header", { class: "task-header" });
|
||||||
const transcript = h("div", { class: "transcript" });
|
const transcript = h("div", { class: "transcript" });
|
||||||
const composer = h("div", { class: "composer" });
|
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 visibleReports = new Map();
|
||||||
const acked = new Set();
|
const acked = new Set();
|
||||||
const ackVisible = () => {
|
const ackVisible = () => {
|
||||||
@@ -171,11 +185,8 @@ export function mountChat(root, src, makeHead) {
|
|||||||
if (running === !spinner.hidden) {
|
if (running === !spinner.hidden) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const pinned = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
|
|
||||||
spinner.hidden = !running;
|
spinner.hidden = !running;
|
||||||
if (pinned) {
|
pin();
|
||||||
transcript.scrollTop = transcript.scrollHeight;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const putBefore = (el, anchor) => {
|
const putBefore = (el, anchor) => {
|
||||||
if (anchor?.parentNode === transcript) {
|
if (anchor?.parentNode === transcript) {
|
||||||
@@ -235,7 +246,8 @@ export function mountChat(root, src, makeHead) {
|
|||||||
pending.push(entry);
|
pending.push(entry);
|
||||||
ctx.run = undefined;
|
ctx.run = undefined;
|
||||||
putBefore(el, spinner);
|
putBefore(el, spinner);
|
||||||
transcript.scrollTop = transcript.scrollHeight;
|
stuck = true;
|
||||||
|
pin();
|
||||||
return () => {
|
return () => {
|
||||||
el.remove();
|
el.remove();
|
||||||
pending = pending.filter((p) => p !== entry);
|
pending = pending.filter((p) => p !== entry);
|
||||||
@@ -299,12 +311,9 @@ export function mountChat(root, src, makeHead) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
stream.timer = 0;
|
stream.timer = 0;
|
||||||
const pinned = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
|
|
||||||
stream.el.hidden = quietReply.startsWith(stream.text.trim());
|
stream.el.hidden = quietReply.startsWith(stream.text.trim());
|
||||||
replace(stream.el, markdown(stream.text, "text md", { taskId: ctx.taskId }));
|
replace(stream.el, markdown(stream.text, "text md", { taskId: ctx.taskId }));
|
||||||
if (pinned) {
|
pin();
|
||||||
transcript.scrollTop = transcript.scrollHeight;
|
|
||||||
}
|
|
||||||
}, 100);
|
}, 100);
|
||||||
};
|
};
|
||||||
const openHandoff = () => {
|
const openHandoff = () => {
|
||||||
@@ -328,7 +337,6 @@ export function mountChat(root, src, makeHead) {
|
|||||||
return handoff.summary;
|
return handoff.summary;
|
||||||
};
|
};
|
||||||
const appendEvents = (events) => {
|
const appendEvents = (events) => {
|
||||||
const pinned = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
|
|
||||||
let contextChanged = false;
|
let contextChanged = false;
|
||||||
headImpl.onEvents?.(events);
|
headImpl.onEvents?.(events);
|
||||||
for (const ev of events) {
|
for (const ev of events) {
|
||||||
@@ -407,11 +415,10 @@ export function mountChat(root, src, makeHead) {
|
|||||||
renderComposer();
|
renderComposer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (pinned) {
|
pin();
|
||||||
transcript.scrollTop = transcript.scrollHeight;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const renderAll = () => {
|
const renderAll = () => {
|
||||||
|
stuck = true;
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
visibleReports.clear();
|
visibleReports.clear();
|
||||||
dropStream();
|
dropStream();
|
||||||
@@ -450,6 +457,8 @@ export function mountChat(root, src, makeHead) {
|
|||||||
document.removeEventListener("visibilitychange", ackVisible);
|
document.removeEventListener("visibilitychange", ackVisible);
|
||||||
headImpl.dispose?.();
|
headImpl.dispose?.();
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
|
resizes.disconnect();
|
||||||
|
mutations.disconnect();
|
||||||
dropStream();
|
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 { findTask, focusedProject, focusTab, on, onEach, state, tabs } from "./state.js";
|
||||||
import { mountPreview } from "./preview.js";
|
import { mountPreview } from "./preview.js";
|
||||||
import { mountProject } from "./project.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 }> = {
|
const views: Record<TaskTab, { label: string; mount: (root: HTMLElement) => () => void }> = {
|
||||||
transcript: { label: "Transcript", mount: mountTranscript },
|
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 transcript = h("div", { class: "transcript" });
|
||||||
const composer = h("div", { class: "composer" });
|
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 visibleReports = new Map<Element, string>();
|
||||||
const acked = new Set<string>();
|
const acked = new Set<string>();
|
||||||
const ackVisible = () => {
|
const ackVisible = () => {
|
||||||
@@ -239,11 +254,8 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
|
|||||||
if (running === !spinner.hidden) {
|
if (running === !spinner.hidden) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const pinned = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
|
|
||||||
spinner.hidden = !running;
|
spinner.hidden = !running;
|
||||||
if (pinned) {
|
pin();
|
||||||
transcript.scrollTop = transcript.scrollHeight;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const putBefore = (el: HTMLElement, anchor: HTMLElement | undefined) => {
|
const putBefore = (el: HTMLElement, anchor: HTMLElement | undefined) => {
|
||||||
if (anchor?.parentNode === transcript) {
|
if (anchor?.parentNode === transcript) {
|
||||||
@@ -309,7 +321,8 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
|
|||||||
pending.push(entry);
|
pending.push(entry);
|
||||||
ctx.run = undefined;
|
ctx.run = undefined;
|
||||||
putBefore(el, spinner);
|
putBefore(el, spinner);
|
||||||
transcript.scrollTop = transcript.scrollHeight;
|
stuck = true;
|
||||||
|
pin();
|
||||||
return () => {
|
return () => {
|
||||||
el.remove();
|
el.remove();
|
||||||
pending = pending.filter((p) => p !== entry);
|
pending = pending.filter((p) => p !== entry);
|
||||||
@@ -376,12 +389,9 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
stream.timer = 0;
|
stream.timer = 0;
|
||||||
const pinned = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
|
|
||||||
stream.el.hidden = quietReply.startsWith(stream.text.trim());
|
stream.el.hidden = quietReply.startsWith(stream.text.trim());
|
||||||
replace(stream.el, markdown(stream.text, "text md", { taskId: ctx.taskId }));
|
replace(stream.el, markdown(stream.text, "text md", { taskId: ctx.taskId }));
|
||||||
if (pinned) {
|
pin();
|
||||||
transcript.scrollTop = transcript.scrollHeight;
|
|
||||||
}
|
|
||||||
}, 100);
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -406,7 +416,6 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
|
|||||||
};
|
};
|
||||||
|
|
||||||
const appendEvents = (events: ByreEvent[]) => {
|
const appendEvents = (events: ByreEvent[]) => {
|
||||||
const pinned = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
|
|
||||||
let contextChanged = false;
|
let contextChanged = false;
|
||||||
headImpl.onEvents?.(events);
|
headImpl.onEvents?.(events);
|
||||||
for (const ev of events) {
|
for (const ev of events) {
|
||||||
@@ -485,12 +494,11 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
|
|||||||
renderComposer();
|
renderComposer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (pinned) {
|
pin();
|
||||||
transcript.scrollTop = transcript.scrollHeight;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderAll = () => {
|
const renderAll = () => {
|
||||||
|
stuck = true;
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
visibleReports.clear();
|
visibleReports.clear();
|
||||||
dropStream();
|
dropStream();
|
||||||
@@ -531,6 +539,8 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
|
|||||||
document.removeEventListener("visibilitychange", ackVisible);
|
document.removeEventListener("visibilitychange", ackVisible);
|
||||||
headImpl.dispose?.();
|
headImpl.dispose?.();
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
|
resizes.disconnect();
|
||||||
|
mutations.disconnect();
|
||||||
dropStream();
|
dropStream();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user