treefmt: apply prettier
Pure `nix fmt` output from the commit before this one — no hand edits. 203 files: 52 md, 42 tsx, 32 js, 32 css, 21 ts, 13 html, 8 json, 3 mjs. Reproduce with `nix develop -c nix fmt` on the parent commit; the result should be byte-identical to this tree. None of the 13 `.prettierignore` entries appears here — verified by intersecting the changed-file list against the ignore file, with a control proving the intersection finds a match when one exists.
This commit is contained in:
parent
5d24bedd60
commit
39b95c2ede
203 changed files with 10090 additions and 6085 deletions
|
|
@ -24,7 +24,8 @@
|
|||
border: 1px solid var(--purple-dim);
|
||||
box-shadow: inset 0 0 24px rgba(0, 0, 0, 0.7);
|
||||
border-radius: 4px;
|
||||
font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", "Source Code Pro", monospace;
|
||||
font-family:
|
||||
"JetBrains Mono", "Fira Code", "Cascadia Code", "Source Code Pro", monospace;
|
||||
font-size: 0.92em;
|
||||
color: var(--fg);
|
||||
margin-top: 0.6em;
|
||||
|
|
@ -64,8 +65,14 @@
|
|||
animation: none;
|
||||
}
|
||||
@keyframes row-fade-in {
|
||||
from { opacity: 0; transform: translateY(4px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
/* Unified prefix column for every row kind. The glyph (`→ ← · ◆ ✓ ✗ ⌁ !`)
|
||||
is the first character of the row's text content; `padding-left` reserves
|
||||
|
|
@ -83,7 +90,9 @@
|
|||
text-indent: -1.4em;
|
||||
margin: 0.1em 0;
|
||||
}
|
||||
.live .row + .row { border-top: 0; }
|
||||
.live .row + .row {
|
||||
border-top: 0;
|
||||
}
|
||||
/* Fixed-width icon column. Rows built with an `icon` (see terminal.js
|
||||
`row()` / `details()`) put it in a `.row-glyph` element instead of as a
|
||||
bare first character. `inline-block` with a fixed `width` means the icon
|
||||
|
|
@ -107,10 +116,20 @@
|
|||
identity (this is a turn boundary, this is a tool call) is carried by
|
||||
the row's icon (`◆`, `🔧`, `✅`, …) and summary text instead of colour —
|
||||
see docs/web-ui/terminal-rendering.md. */
|
||||
.live .level-debug { color: var(--muted); }
|
||||
.live .level-info { color: var(--fg); }
|
||||
.live .level-warn { color: var(--amber); border-left-color: var(--amber); }
|
||||
.live .level-error { color: var(--red); border-left-color: var(--red); }
|
||||
.live .level-debug {
|
||||
color: var(--muted);
|
||||
}
|
||||
.live .level-info {
|
||||
color: var(--fg);
|
||||
}
|
||||
.live .level-warn {
|
||||
color: var(--amber);
|
||||
border-left-color: var(--amber);
|
||||
}
|
||||
.live .level-error {
|
||||
color: var(--red);
|
||||
border-left-color: var(--red);
|
||||
}
|
||||
/* `badge-pulse` itself is no longer used by any terminal row (the
|
||||
turn-start `unread` count it animated is gone — see term_msg.rs's
|
||||
module doc), but agent.css's `.state-badge.state-thinking`/
|
||||
|
|
@ -118,13 +137,23 @@
|
|||
`@import "@hive/shared/terminal.css"` — keep the definition here, drop
|
||||
only the terminal-specific `.unread-badge` selector that used it. */
|
||||
@keyframes badge-pulse {
|
||||
0%, 100% { opacity: 1; text-shadow: 0 0 6px color-mix(in srgb, var(--amber) 55%, transparent); }
|
||||
50% { opacity: 0.7; text-shadow: 0 0 14px color-mix(in srgb, var(--amber) 95%, transparent); }
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 6px color-mix(in srgb, var(--amber) 55%, transparent);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
text-shadow: 0 0 14px color-mix(in srgb, var(--amber) 95%, transparent);
|
||||
}
|
||||
}
|
||||
/* Any child block (markdown body, nested details) resets the parent
|
||||
row's hanging indent so the content lays out from column 0 of the
|
||||
body area. */
|
||||
.live .row .md, .live .row > details { text-indent: 0; }
|
||||
.live .row .md,
|
||||
.live .row > details {
|
||||
text-indent: 0;
|
||||
}
|
||||
/* "↓ N new" pill: shown when new rows arrive while the operator is
|
||||
scrolled up; click to jump to bottom. Positioned by the wrapper's
|
||||
`position: relative` (terminal-wrap supplies it; pages that skip the
|
||||
|
|
@ -147,14 +176,18 @@
|
|||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
pointer-events: none;
|
||||
transition: opacity 160ms ease, transform 160ms ease;
|
||||
transition:
|
||||
opacity 160ms ease,
|
||||
transform 160ms ease;
|
||||
}
|
||||
.tail-pill.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
.tail-pill:hover { filter: brightness(1.1); }
|
||||
.tail-pill:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
/* "↑ load older" pill: sits inline at the top of the log (not
|
||||
absolutely positioned) so it scrolls with the content. Appears
|
||||
when `has_more` is true after initial history load. */
|
||||
|
|
@ -172,7 +205,9 @@
|
|||
padding: 0.4em 1em;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: color 120ms ease, background 120ms ease;
|
||||
transition:
|
||||
color 120ms ease,
|
||||
background 120ms ease;
|
||||
}
|
||||
.load-more-pill:hover:not(:disabled) {
|
||||
color: var(--fg);
|
||||
|
|
@ -235,10 +270,12 @@ details.row > summary > .summary-text:only-child {
|
|||
text-indent: -1.4em;
|
||||
}
|
||||
details.row > summary > .summary-text::before {
|
||||
content: '▸ ';
|
||||
content: "▸ ";
|
||||
color: inherit;
|
||||
}
|
||||
details.row[open] > summary > .summary-text::before { content: '▾ '; }
|
||||
details.row[open] > summary > .summary-text::before {
|
||||
content: "▾ ";
|
||||
}
|
||||
details.row > pre.diff-body,
|
||||
details.row > pre.tool-body {
|
||||
margin: 0.3em 0 0.4em 0;
|
||||
|
|
@ -251,16 +288,30 @@ details.row > pre.tool-body {
|
|||
max-height: 22em;
|
||||
overflow-y: auto;
|
||||
}
|
||||
details.row > pre.tool-body { color: var(--fg); }
|
||||
details.row > pre.diff-body .diff-add { color: var(--green); }
|
||||
details.row > pre.diff-body .diff-del { color: var(--red); }
|
||||
details.row > pre.diff-body .diff-ctx { color: var(--fg); }
|
||||
details.row > pre.tool-body {
|
||||
color: var(--fg);
|
||||
}
|
||||
details.row > pre.diff-body .diff-add {
|
||||
color: var(--green);
|
||||
}
|
||||
details.row > pre.diff-body .diff-del {
|
||||
color: var(--red);
|
||||
}
|
||||
details.row > pre.diff-body .diff-ctx {
|
||||
color: var(--fg);
|
||||
}
|
||||
/* Markdown body inside a row (assistant text, send/recv message
|
||||
bodies). Inline elements get muted accents; block elements
|
||||
reset the parent row's hanging indent so content lays out cleanly. */
|
||||
.live .row .md p { margin: 0.2em 0; }
|
||||
.live .row .md p:first-child { margin-top: 0; }
|
||||
.live .row .md p:last-child { margin-bottom: 0; }
|
||||
.live .row .md p {
|
||||
margin: 0.2em 0;
|
||||
}
|
||||
.live .row .md p:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.live .row .md p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.live .row .md code {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
padding: 0.05em 0.3em;
|
||||
|
|
@ -281,14 +332,34 @@ details.row > pre.diff-body .diff-ctx { color: var(--fg); }
|
|||
padding: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
.live .row .md a { color: var(--cyan); text-decoration: underline; }
|
||||
.live .row .md a {
|
||||
color: var(--cyan);
|
||||
text-decoration: underline;
|
||||
}
|
||||
/* Auto-linkified bare URLs in plain rows + tool-body blocks. */
|
||||
.live .row a { color: var(--cyan); text-decoration: underline; }
|
||||
.live .row a:hover { color: var(--fg); }
|
||||
.live .row .md strong { color: inherit; font-weight: bold; }
|
||||
.live .row .md em { color: inherit; font-style: italic; }
|
||||
.live .row .md ul, .live .row .md ol { margin: 0.2em 0 0.2em 1.4em; padding: 0; }
|
||||
.live .row .md li { margin: 0.05em 0; }
|
||||
.live .row a {
|
||||
color: var(--cyan);
|
||||
text-decoration: underline;
|
||||
}
|
||||
.live .row a:hover {
|
||||
color: var(--fg);
|
||||
}
|
||||
.live .row .md strong {
|
||||
color: inherit;
|
||||
font-weight: bold;
|
||||
}
|
||||
.live .row .md em {
|
||||
color: inherit;
|
||||
font-style: italic;
|
||||
}
|
||||
.live .row .md ul,
|
||||
.live .row .md ol {
|
||||
margin: 0.2em 0 0.2em 1.4em;
|
||||
padding: 0;
|
||||
}
|
||||
.live .row .md li {
|
||||
margin: 0.05em 0;
|
||||
}
|
||||
.live .row .md blockquote {
|
||||
margin: 0.2em 0;
|
||||
padding-left: 0.6em;
|
||||
|
|
|
|||
|
|
@ -23,11 +23,11 @@ export function create(opts) {
|
|||
// row/details/etc. into a detached temp element while prepending older
|
||||
// history (restored before any scrollTop adjustments).
|
||||
let log = opts.logEl;
|
||||
if (!log) throw new Error('HiveTerminal.create: logEl is required');
|
||||
if (!log) throw new Error("HiveTerminal.create: logEl is required");
|
||||
const rootLog = log; // always the real DOM element — never reassigned
|
||||
const renderers = opts.renderers || {};
|
||||
const defaultRender = renderers._default
|
||||
|| ((ev, api) => api.row('note', JSON.stringify(ev)));
|
||||
const defaultRender =
|
||||
renderers._default || ((ev, api) => api.row("note", JSON.stringify(ev)));
|
||||
const pillAnchor = opts.pillAnchor || log.parentElement || log;
|
||||
|
||||
let placeholderEl = null;
|
||||
|
|
@ -55,7 +55,9 @@ export function create(opts) {
|
|||
let scrollAnimRaf = 0;
|
||||
|
||||
function isNearBottom() {
|
||||
return log.scrollHeight - log.scrollTop - log.clientHeight <= NEAR_BOTTOM_PX;
|
||||
return (
|
||||
log.scrollHeight - log.scrollTop - log.clientHeight <= NEAR_BOTTOM_PX
|
||||
);
|
||||
}
|
||||
// Snap the log to the bottom with a brief eased animation. Cancels
|
||||
// any in-flight frame loop so back-to-back snaps coalesce; falls
|
||||
|
|
@ -99,23 +101,23 @@ export function create(opts) {
|
|||
}
|
||||
function ensurePill() {
|
||||
if (pill) return pill;
|
||||
pill = document.createElement('button');
|
||||
pill.type = 'button';
|
||||
pill.className = 'tail-pill';
|
||||
pill.addEventListener('click', () => snapToBottom());
|
||||
pill = document.createElement("button");
|
||||
pill.type = "button";
|
||||
pill.className = "tail-pill";
|
||||
pill.addEventListener("click", () => snapToBottom());
|
||||
pillAnchor.appendChild(pill);
|
||||
return pill;
|
||||
}
|
||||
function updatePill() {
|
||||
if (unseen <= 0) {
|
||||
if (pill) pill.classList.remove('visible');
|
||||
if (pill) pill.classList.remove("visible");
|
||||
return;
|
||||
}
|
||||
ensurePill();
|
||||
pill.textContent = '↓ ' + unseen + ' new';
|
||||
pill.classList.add('visible');
|
||||
pill.textContent = "↓ " + unseen + " new";
|
||||
pill.classList.add("visible");
|
||||
}
|
||||
log.addEventListener('scroll', () => {
|
||||
log.addEventListener("scroll", () => {
|
||||
// Sticky-bottom intent tracking. Outside an animation this is
|
||||
// straightforward — stickToBottom = isNearBottom(). During a
|
||||
// smooth-snap animation we swallow most of the event to avoid a
|
||||
|
|
@ -137,11 +139,18 @@ export function create(opts) {
|
|||
// MO stops calling snapToBottom() and the gate expires.
|
||||
stickToBottom = false;
|
||||
}
|
||||
if (stickToBottom) { unseen = 0; updatePill(); }
|
||||
if (stickToBottom) {
|
||||
unseen = 0;
|
||||
updatePill();
|
||||
}
|
||||
if (inAnim) return;
|
||||
// Auto-fetch older history when the user scrolls near the top — no
|
||||
// click required; the load-more pill stays as a visual indicator.
|
||||
if (rootLog.scrollTop <= LOAD_MORE_SCROLL_PX && histHasMore && !histLoading) {
|
||||
if (
|
||||
rootLog.scrollTop <= LOAD_MORE_SCROLL_PX &&
|
||||
histHasMore &&
|
||||
!histLoading
|
||||
) {
|
||||
loadMore();
|
||||
}
|
||||
});
|
||||
|
|
@ -174,8 +183,8 @@ export function create(opts) {
|
|||
}
|
||||
function placeholder(text) {
|
||||
clearPlaceholder();
|
||||
const e = document.createElement('div');
|
||||
e.className = 'row note';
|
||||
const e = document.createElement("div");
|
||||
e.className = "row note";
|
||||
e.textContent = text;
|
||||
log.appendChild(e);
|
||||
placeholderEl = e;
|
||||
|
|
@ -186,8 +195,8 @@ export function create(opts) {
|
|||
// Optional: callers that pass no `icon` keep the bare first-character
|
||||
// prefix the older rows rely on.
|
||||
function glyphSpan(icon) {
|
||||
const g = document.createElement('span');
|
||||
g.className = 'row-glyph';
|
||||
const g = document.createElement("span");
|
||||
g.className = "row-glyph";
|
||||
g.textContent = icon;
|
||||
return g;
|
||||
}
|
||||
|
|
@ -196,10 +205,10 @@ export function create(opts) {
|
|||
// caret (CSS `.summary-text::before`) then leads the text, not the icon,
|
||||
// so the icon stays aligned with flat-row icons.
|
||||
function buildSummary(summary, icon) {
|
||||
const s = document.createElement('summary');
|
||||
if (icon != null && icon !== '') s.appendChild(glyphSpan(icon));
|
||||
const st = document.createElement('span');
|
||||
st.className = 'summary-text';
|
||||
const s = document.createElement("summary");
|
||||
if (icon != null && icon !== "") s.appendChild(glyphSpan(icon));
|
||||
const st = document.createElement("span");
|
||||
st.className = "summary-text";
|
||||
st.textContent = summary;
|
||||
s.appendChild(st);
|
||||
return s;
|
||||
|
|
@ -207,9 +216,9 @@ export function create(opts) {
|
|||
function row(cls, text, icon) {
|
||||
clearPlaceholder();
|
||||
const wasNearBottom = isNearBottom();
|
||||
const e = document.createElement('div');
|
||||
e.className = 'row ' + (cls || '') + (currentNoAnim ? ' no-anim' : '');
|
||||
if (icon != null && icon !== '') e.appendChild(glyphSpan(icon));
|
||||
const e = document.createElement("div");
|
||||
e.className = "row " + (cls || "") + (currentNoAnim ? " no-anim" : "");
|
||||
if (icon != null && icon !== "") e.appendChild(glyphSpan(icon));
|
||||
e.appendChild(linkify(text));
|
||||
log.appendChild(e);
|
||||
afterAppend(wasNearBottom);
|
||||
|
|
@ -222,10 +231,10 @@ export function create(opts) {
|
|||
function mutableRow(cls, text, icon) {
|
||||
clearPlaceholder();
|
||||
const wasNearBottom = isNearBottom();
|
||||
const e = document.createElement('div');
|
||||
e.className = 'row ' + (cls || '') + (currentNoAnim ? ' no-anim' : '');
|
||||
if (icon != null && icon !== '') e.appendChild(glyphSpan(icon));
|
||||
const tn = document.createTextNode(text == null ? '' : String(text));
|
||||
const e = document.createElement("div");
|
||||
e.className = "row " + (cls || "") + (currentNoAnim ? " no-anim" : "");
|
||||
if (icon != null && icon !== "") e.appendChild(glyphSpan(icon));
|
||||
const tn = document.createTextNode(text == null ? "" : String(text));
|
||||
e.appendChild(tn);
|
||||
log.appendChild(e);
|
||||
afterAppend(wasNearBottom);
|
||||
|
|
@ -240,17 +249,19 @@ export function create(opts) {
|
|||
// already set `d.open = true` themselves after calling this — this only
|
||||
// changes the *default* for panels that would otherwise start closed.
|
||||
function wantsExpandedDefault() {
|
||||
return typeof opts.expandDetails === 'function' ? !!opts.expandDetails() : !!opts.expandDetails;
|
||||
return typeof opts.expandDetails === "function"
|
||||
? !!opts.expandDetails()
|
||||
: !!opts.expandDetails;
|
||||
}
|
||||
function details(cls, summary, body, icon) {
|
||||
clearPlaceholder();
|
||||
const wasNearBottom = isNearBottom();
|
||||
const d = document.createElement('details');
|
||||
d.className = 'row ' + (cls || '') + (currentNoAnim ? ' no-anim' : '');
|
||||
const d = document.createElement("details");
|
||||
d.className = "row " + (cls || "") + (currentNoAnim ? " no-anim" : "");
|
||||
if (wantsExpandedDefault()) d.open = true;
|
||||
d.appendChild(buildSummary(summary, icon));
|
||||
const pre = document.createElement('pre');
|
||||
pre.className = 'tool-body';
|
||||
const pre = document.createElement("pre");
|
||||
pre.className = "tool-body";
|
||||
pre.appendChild(linkify(body));
|
||||
d.appendChild(pre);
|
||||
log.appendChild(d);
|
||||
|
|
@ -260,18 +271,18 @@ export function create(opts) {
|
|||
function detailsDiff(cls, summary, body, icon) {
|
||||
clearPlaceholder();
|
||||
const wasNearBottom = isNearBottom();
|
||||
const d = document.createElement('details');
|
||||
d.className = 'row ' + (cls || '') + (currentNoAnim ? ' no-anim' : '');
|
||||
const d = document.createElement("details");
|
||||
d.className = "row " + (cls || "") + (currentNoAnim ? " no-anim" : "");
|
||||
if (wantsExpandedDefault()) d.open = true;
|
||||
d.appendChild(buildSummary(summary, icon));
|
||||
const pre = document.createElement('pre');
|
||||
pre.className = 'tool-body diff-body';
|
||||
for (const line of String(body).split('\n')) {
|
||||
const span = document.createElement('span');
|
||||
if (line.startsWith('+ ')) span.className = 'diff-add';
|
||||
else if (line.startsWith('- ')) span.className = 'diff-del';
|
||||
else span.className = 'diff-ctx';
|
||||
span.textContent = line + '\n';
|
||||
const pre = document.createElement("pre");
|
||||
pre.className = "tool-body diff-body";
|
||||
for (const line of String(body).split("\n")) {
|
||||
const span = document.createElement("span");
|
||||
if (line.startsWith("+ ")) span.className = "diff-add";
|
||||
else if (line.startsWith("- ")) span.className = "diff-del";
|
||||
else span.className = "diff-ctx";
|
||||
span.textContent = line + "\n";
|
||||
pre.appendChild(span);
|
||||
}
|
||||
d.appendChild(pre);
|
||||
|
|
@ -281,22 +292,33 @@ export function create(opts) {
|
|||
}
|
||||
|
||||
function api(extra) {
|
||||
return Object.assign({
|
||||
row, mutableRow, details, detailsDiff, placeholder, linkify,
|
||||
fromHistory: false,
|
||||
}, extra || {});
|
||||
return Object.assign(
|
||||
{
|
||||
row,
|
||||
mutableRow,
|
||||
details,
|
||||
detailsDiff,
|
||||
placeholder,
|
||||
linkify,
|
||||
fromHistory: false,
|
||||
},
|
||||
extra || {},
|
||||
);
|
||||
}
|
||||
function dispatch(ev, fromHistory) {
|
||||
const r = renderers[ev.kind] || defaultRender;
|
||||
try {
|
||||
r(ev, api({ fromHistory }));
|
||||
} catch (err) {
|
||||
console.error('terminal renderer threw', ev, err);
|
||||
row('note', '[render err] ' + (err && err.message ? err.message : err));
|
||||
console.error("terminal renderer threw", ev, err);
|
||||
row("note", "[render err] " + (err && err.message ? err.message : err));
|
||||
}
|
||||
if (opts.onAnyEvent) {
|
||||
try { opts.onAnyEvent(ev, { fromHistory }); }
|
||||
catch (err) { console.error('onAnyEvent threw', err); }
|
||||
try {
|
||||
opts.onAnyEvent(ev, { fromHistory });
|
||||
} catch (err) {
|
||||
console.error("onAnyEvent threw", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -317,29 +339,36 @@ export function create(opts) {
|
|||
return;
|
||||
}
|
||||
if (!loadMoreBtn) {
|
||||
loadMoreBtn = document.createElement('button');
|
||||
loadMoreBtn.type = 'button';
|
||||
loadMoreBtn.className = 'load-more-pill';
|
||||
loadMoreBtn.addEventListener('click', loadMore);
|
||||
loadMoreBtn = document.createElement("button");
|
||||
loadMoreBtn.type = "button";
|
||||
loadMoreBtn.className = "load-more-pill";
|
||||
loadMoreBtn.addEventListener("click", loadMore);
|
||||
rootLog.prepend(loadMoreBtn);
|
||||
}
|
||||
loadMoreBtn.textContent = '↑ load older';
|
||||
loadMoreBtn.textContent = "↑ load older";
|
||||
loadMoreBtn.disabled = false;
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (!histHasMore || histLoading || !opts.historyUrl || histMinId === null) return;
|
||||
if (!histHasMore || histLoading || !opts.historyUrl || histMinId === null)
|
||||
return;
|
||||
histLoading = true;
|
||||
if (loadMoreBtn) { loadMoreBtn.textContent = '↑ loading…'; loadMoreBtn.disabled = true; }
|
||||
if (loadMoreBtn) {
|
||||
loadMoreBtn.textContent = "↑ loading…";
|
||||
loadMoreBtn.disabled = true;
|
||||
}
|
||||
try {
|
||||
const sep = opts.historyUrl.includes('?') ? '&' : '?';
|
||||
const url = opts.historyUrl + sep + 'before=' + histMinId;
|
||||
const sep = opts.historyUrl.includes("?") ? "&" : "?";
|
||||
const url = opts.historyUrl + sep + "before=" + histMinId;
|
||||
const resp = await fetch(url);
|
||||
if (!resp.ok) { updateLoadMoreBtn(); return; }
|
||||
if (!resp.ok) {
|
||||
updateLoadMoreBtn();
|
||||
return;
|
||||
}
|
||||
const body = await resp.json();
|
||||
const events = Array.isArray(body) ? body : (body.events || []);
|
||||
const events = Array.isArray(body) ? body : body.events || [];
|
||||
histHasMore = body.has_more || false;
|
||||
if (typeof body.min_id === 'number') histMinId = body.min_id;
|
||||
if (typeof body.min_id === "number") histMinId = body.min_id;
|
||||
|
||||
// Resolve load-more button state before capturing the scroll
|
||||
// baseline so that any button removal is already reflected in
|
||||
|
|
@ -350,7 +379,7 @@ export function create(opts) {
|
|||
if (events.length > 0) {
|
||||
// Render into a detached element; `log` is temporarily redirected
|
||||
// so that row/details/etc. append there instead of rootLog.
|
||||
const tempEl = document.createElement('div');
|
||||
const tempEl = document.createElement("div");
|
||||
log = tempEl;
|
||||
currentNoAnim = true;
|
||||
for (const ev of events) dispatch(ev, true);
|
||||
|
|
@ -358,23 +387,26 @@ export function create(opts) {
|
|||
log = rootLog;
|
||||
|
||||
// Separator to mark the boundary between loaded-older and newer.
|
||||
const sepEl = document.createElement('div');
|
||||
sepEl.className = 'row note no-anim';
|
||||
sepEl.textContent = '─── older above ───';
|
||||
const sepEl = document.createElement("div");
|
||||
sepEl.className = "row note no-anim";
|
||||
sepEl.textContent = "─── older above ───";
|
||||
tempEl.appendChild(sepEl);
|
||||
|
||||
// Insert before the "live" divider (i.e. right after the load-more
|
||||
// button if present, else at the very top of rootLog).
|
||||
const anchor = loadMoreBtn ? loadMoreBtn.nextSibling : rootLog.firstChild;
|
||||
const anchor = loadMoreBtn
|
||||
? loadMoreBtn.nextSibling
|
||||
: rootLog.firstChild;
|
||||
const beforeH = rootLog.scrollHeight;
|
||||
while (tempEl.firstChild) rootLog.insertBefore(tempEl.firstChild, anchor);
|
||||
while (tempEl.firstChild)
|
||||
rootLog.insertBefore(tempEl.firstChild, anchor);
|
||||
// Compensate scroll so the viewport stays on the same content.
|
||||
// overflow-anchor: none on .live ensures the browser does not
|
||||
// also auto-adjust scrollTop (which would double the delta).
|
||||
rootLog.scrollTop += rootLog.scrollHeight - beforeH;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('loadMore failed', err);
|
||||
console.warn("loadMore failed", err);
|
||||
updateLoadMoreBtn();
|
||||
} finally {
|
||||
histLoading = false;
|
||||
|
|
@ -414,21 +446,31 @@ export function create(opts) {
|
|||
: new EventSource(opts.streamUrl);
|
||||
es.onmessage = (e) => {
|
||||
let ev;
|
||||
try { ev = JSON.parse(e.data); }
|
||||
catch (err) { row('note', '[parse err] ' + e.data); return; }
|
||||
if (!live) { buffered.push(ev); return; }
|
||||
try {
|
||||
ev = JSON.parse(e.data);
|
||||
} catch (err) {
|
||||
row("note", "[parse err] " + e.data);
|
||||
return;
|
||||
}
|
||||
if (!live) {
|
||||
buffered.push(ev);
|
||||
return;
|
||||
}
|
||||
dispatch(ev, false);
|
||||
if (opts.onLiveEvent) {
|
||||
try { opts.onLiveEvent(ev); }
|
||||
catch (err) { console.error('onLiveEvent threw', err); }
|
||||
try {
|
||||
opts.onLiveEvent(ev);
|
||||
} catch (err) {
|
||||
console.error("onLiveEvent threw", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
es.onerror = () => {
|
||||
// SharedWorker-backed facades expose `readyState` mirroring the
|
||||
// upstream EventSource state; the native EventSource exposes the
|
||||
// same. Either way the CONNECTING vs. closed distinction works.
|
||||
if (es.readyState === 0 /* CONNECTING */) row('note', '[reconnecting…]');
|
||||
else row('note', '[disconnected]');
|
||||
if (es.readyState === 0 /* CONNECTING */) row("note", "[reconnecting…]");
|
||||
else row("note", "[disconnected]");
|
||||
};
|
||||
es.onopen = () => {
|
||||
// Fires on the initial connect and on every automatic
|
||||
|
|
@ -438,8 +480,11 @@ export function create(opts) {
|
|||
// must re-sync here or it shows stale state until a manual
|
||||
// reload.
|
||||
if (opts.onStreamOpen) {
|
||||
try { opts.onStreamOpen(); }
|
||||
catch (err) { console.error('onStreamOpen threw', err); }
|
||||
try {
|
||||
opts.onStreamOpen();
|
||||
} catch (err) {
|
||||
console.error("onStreamOpen threw", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -456,15 +501,22 @@ export function create(opts) {
|
|||
// broker-history seq would wrongly drop ones that fired
|
||||
// between a consumer's own snapshot read and this history
|
||||
// fetch. ev.seq absent/0 → no dedupe possible.
|
||||
if (boundarySeq != null
|
||||
&& typeof ev.seq === 'number' && ev.seq <= boundarySeq
|
||||
&& historyKinds && historyKinds.has(ev.kind)) {
|
||||
if (
|
||||
boundarySeq != null &&
|
||||
typeof ev.seq === "number" &&
|
||||
ev.seq <= boundarySeq &&
|
||||
historyKinds &&
|
||||
historyKinds.has(ev.kind)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
dispatch(ev, false);
|
||||
if (opts.onLiveEvent) {
|
||||
try { opts.onLiveEvent(ev); }
|
||||
catch (err) { console.error('onLiveEvent threw', err); }
|
||||
try {
|
||||
opts.onLiveEvent(ev);
|
||||
} catch (err) {
|
||||
console.error("onLiveEvent threw", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -486,12 +538,12 @@ export function create(opts) {
|
|||
// Accept the envelope `{ seq, events, min_id?, has_more? }`.
|
||||
// A bare array means the server hasn't been updated — treat it
|
||||
// as "no dedupe possible, no pagination."
|
||||
const events = Array.isArray(body) ? body : (body.events || []);
|
||||
const events = Array.isArray(body) ? body : body.events || [];
|
||||
const boundarySeq = Array.isArray(body) ? null : (body.seq ?? null);
|
||||
// Pagination cursors — set on the outer load-more state.
|
||||
if (!Array.isArray(body)) {
|
||||
histHasMore = body.has_more || false;
|
||||
if (typeof body.min_id === 'number') histMinId = body.min_id;
|
||||
if (typeof body.min_id === "number") histMinId = body.min_id;
|
||||
}
|
||||
// Kinds present in the history replay — the only kinds that
|
||||
// can double and therefore the only ones to seq-dedupe.
|
||||
|
|
@ -499,14 +551,14 @@ export function create(opts) {
|
|||
currentNoAnim = true;
|
||||
for (const ev of events) dispatch(ev, true);
|
||||
currentNoAnim = false;
|
||||
if (events.length) row('note', '─── live (older above) ───');
|
||||
else placeholder('(connected — waiting for events)');
|
||||
if (events.length) row("note", "─── live (older above) ───");
|
||||
else placeholder("(connected — waiting for events)");
|
||||
flushBuffered(boundarySeq, historyKinds);
|
||||
// Show load-older button if the server reports more history.
|
||||
updateLoadMoreBtn();
|
||||
if (opts.onBackfillDone) opts.onBackfillDone(events.length);
|
||||
} catch (err) {
|
||||
console.warn('history backfill failed', err);
|
||||
console.warn("history backfill failed", err);
|
||||
flushBuffered(null);
|
||||
if (opts.onBackfillDone) opts.onBackfillDone(0);
|
||||
}
|
||||
|
|
@ -524,9 +576,10 @@ export function create(opts) {
|
|||
// XSS-safety + trailing-punctuation strip.
|
||||
const LINKIFY_URL_RE = /https?:\/\/[^\s<>"']+/g;
|
||||
export function linkify(text) {
|
||||
const str = text == null ? '' : String(text);
|
||||
const str = text == null ? "" : String(text);
|
||||
const frag = document.createDocumentFragment();
|
||||
if (str.indexOf('://') === -1) { // fast path: no URLs
|
||||
if (str.indexOf("://") === -1) {
|
||||
// fast path: no URLs
|
||||
if (str) frag.appendChild(document.createTextNode(str));
|
||||
return frag;
|
||||
}
|
||||
|
|
@ -537,20 +590,20 @@ export function linkify(text) {
|
|||
let url = m[0];
|
||||
// Don't swallow trailing punctuation that's really sentence text.
|
||||
const trail = url.match(/[.,;:!?)\]}'"]+$/);
|
||||
const tail = trail ? trail[0] : '';
|
||||
const tail = trail ? trail[0] : "";
|
||||
if (tail) url = url.slice(0, -tail.length);
|
||||
if (m.index > last) {
|
||||
frag.appendChild(document.createTextNode(str.slice(last, m.index)));
|
||||
}
|
||||
if (!url.slice(url.indexOf('://') + 3)) {
|
||||
if (!url.slice(url.indexOf("://") + 3)) {
|
||||
// Nothing past the scheme — not a real URL, emit verbatim.
|
||||
frag.appendChild(document.createTextNode(m[0]));
|
||||
} else {
|
||||
const a = document.createElement('a');
|
||||
a.href = url; // regex only matches https?:// — safe
|
||||
const a = document.createElement("a");
|
||||
a.href = url; // regex only matches https?:// — safe
|
||||
a.textContent = url;
|
||||
a.target = '_blank';
|
||||
a.rel = 'noopener noreferrer';
|
||||
a.target = "_blank";
|
||||
a.rel = "noopener noreferrer";
|
||||
frag.appendChild(a);
|
||||
if (tail) frag.appendChild(document.createTextNode(tail));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue