Mark unread turn summaries in the switcher and Project rows

This commit is contained in:
Ian Gulliver
2026-10-16 11:08:57 -07:00
parent ec169583ff
commit 34db128afe
24 changed files with 755 additions and 239 deletions
+56 -1
View File
@@ -4,7 +4,7 @@ import { markdown } from "./markdown.js";
import { previewURL } from "./preview.js";
import { findingCount, reviewCounts, severityCounts, unignoredFindings } from "./reviews.js";
import { composerText, endSend, flushDrafts, forgetLostImages, lostImageCount, setComposerImages, setComposerText, startSend } from "./drafts.js";
import { currentSession, findTask, focusTab, focusTask, on, onEach, pickReview, pinSnapshot, setFocus, showReview, state, transcriptLoading, type Topic } from "./state.js";
import { currentSession, findTask, focusTab, focusTask, markSummaryRead, on, onEach, pickReview, pinSnapshot, setFocus, showReview, state, transcriptLoading, type Topic } from "./state.js";
import { taskStyle } from "./taskcolor.js";
import { fillTodoLinks } from "./todolink.js";
@@ -220,6 +220,51 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
const mutations = new MutationObserver(pin);
mutations.observe(transcript, { childList: true, subtree: true, characterData: true });
const shown = new Set<HTMLElement>();
const markShown = () => {
const taskId = ctx.taskId;
if (!taskId || document.visibilityState !== "visible") {
return;
}
for (const el of shown) {
markSummaryRead(taskId, Number(el.dataset["seq"]));
}
};
const sightings = new IntersectionObserver(
(entries) => {
for (const e of entries) {
if (e.isIntersecting) {
shown.add(e.target as HTMLElement);
} else {
shown.delete(e.target as HTMLElement);
}
}
markShown();
},
{ root: transcript },
);
document.addEventListener("visibilitychange", markShown);
const jump = () => {
const target = state.jump;
if (!target || target.taskId !== ctx.taskId) {
return;
}
const end = transcript.querySelector<HTMLElement>(`.turn-end[data-seq="${target.seq}"]`);
if (!end) {
return;
}
state.jump = undefined;
const msg = end.previousElementSibling;
const box = transcript.getBoundingClientRect();
if (msg?.classList.contains("agent")) {
transcript.scrollTop += msg.getBoundingClientRect().top - box.top - 8;
} else {
transcript.scrollTop += end.getBoundingClientRect().bottom - box.bottom + 8;
}
stuck = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
};
const newCtx = (): Ctx => ({
tools: new Map(),
subagents: new Map(),
@@ -517,6 +562,10 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
if (ev.kind === "user.message" && p["mid_turn"] !== true) {
ctx.open = true;
}
if (ev.kind === "turn.ended" && p["summary"] === true && ctx.taskId) {
el.dataset["seq"] = String(ev.seq);
sightings.observe(el);
}
put(el);
}
syncRunning();
@@ -529,12 +578,15 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
}
fillTodoLinks(transcript, models[0]!);
pin();
jump();
};
const renderAll = () => {
stuck = true;
dropStream();
pending = [];
sightings.disconnect();
shown.clear();
ctx = newCtx();
context = new Map();
focusStart = true;
@@ -563,6 +615,7 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
onEach([src.streamTopic], (ev) => appendDelta(ev as ByreEvent)),
onEach([src.readyTopic], () => loading.remove()),
onEach(["tasks", "sessions"], () => fillTodoLinks(transcript, models[0]!)),
on("jump", jump),
];
replace(root, h("div", { class: "transcript-view" }, head, transcript, composer));
@@ -574,6 +627,8 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
headImpl.dispose?.();
resizes.disconnect();
mutations.disconnect();
sightings.disconnect();
document.removeEventListener("visibilitychange", markShown);
dropStream();
const input = composer.querySelector("textarea");
if (input) {