A task now opens at its newest step instead of where the previous task was scrolled to, and a timeline read at its foot follows new steps whatever the task's state (before, only while a step was running). Read further up, it stays put; a round button over the message box leads back down, with the count of the AI's new steps on its corner. It replaces the "new steps below" pill (ui_ai_newest.js). The side column's grip lay over the timeline's scrollbar, so grabbing the scrollbar often resized the column instead. It now has a gap of its own beside the scrollbar, drawn as three dots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
47 lines
2.0 KiB
JavaScript
47 lines
2.0 KiB
JavaScript
/**
|
|
* The way back to a task's newest step, over the message box (ui_ai_workbench.js puts it there): a
|
|
* round button, there while the user reads more than a screen above the timeline's foot or the AI's
|
|
* steps came in since they left it, with how many on its corner; a click takes them down. atFoot
|
|
* tells the workbench whether a redraw keeps the timeline at its foot.
|
|
*/
|
|
|
|
import { translate as t } from './locales.js';
|
|
import { icon } from './ui_ai_icons.js';
|
|
|
|
const NEAR_BOTTOM = 60;
|
|
|
|
const below = scroller => scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight;
|
|
|
|
/** Whether `scroller` is scrolled to (about) its foot. */
|
|
export const atFoot = scroller => below(scroller) < NEAR_BOTTOM;
|
|
|
|
/** The button for the timeline in `scroller`; `arrived(count)` after a redraw that brought the AI's new steps. */
|
|
export function newestButton(scroller) {
|
|
const button = document.createElement('button');
|
|
button.type = 'button';
|
|
button.className = 'anomalous-ai-newest';
|
|
button.hidden = true;
|
|
const count = document.createElement('span');
|
|
count.className = 'anomalous-ai-newest-count';
|
|
button.append(icon('chevronDown', 18), count);
|
|
let unseen = 0;
|
|
const draw = () => {
|
|
if (atFoot(scroller)) unseen = 0;
|
|
button.hidden = !unseen && below(scroller) < scroller.clientHeight;
|
|
count.textContent = unseen > 99 ? '99+' : unseen ? String(unseen) : '';
|
|
button.title = unseen ? t('aiNewSteps', { count: unseen }) : t('aiToNewest');
|
|
button.setAttribute('aria-label', button.title);
|
|
};
|
|
button.onclick = () => scroller.scrollTo({ top: scroller.scrollHeight, behavior: 'smooth' });
|
|
const scrolled = () => {
|
|
if (!button.isConnected) scroller.removeEventListener('scroll', scrolled); // another task's view by now
|
|
else draw();
|
|
};
|
|
scroller.addEventListener('scroll', scrolled, { passive: true });
|
|
button.arrived = (steps) => {
|
|
if (!atFoot(scroller)) unseen += steps;
|
|
draw();
|
|
};
|
|
return button;
|
|
}
|