Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

71 linesCodeBlameRaw
1/**
2 * When messages shown outside their conversation count as read: g1t's
3 * answer on Home's Summarize card, the top bar's Ask panel, or any other
4 * page that shows a conversation's messages. Pure rules, and the record of
5 * which conversations this page shows right now, so a toast, a sound or a
6 * system notification does not repeat what is on screen (lib/notify-client,
7 * lib/sound-events). The hook that uses them is lib/use-read-when-shown.ts.
8 */
9import type { ChatMessage } from "@g1t/contracts";
10
11/** What a surface knows when it might mark its conversation read. */
12export type ReadMoment = {
13 /** `document.visibilityState === "visible"`. */
14 visible: boolean;
15 /** The messages are on screen: the panel open, the card scrolled into view. */
16 onScreen: boolean;
17 /** The newest message shown and finished, or null while there is none. */
18 newestId: string | null;
19 /** The newest this surface has already marked read. */
20 lastRead: string | null;
21};
22
23/**
24 * The message to mark read now, or null: only while the page is visible
25 * and the messages are on screen, each newest id once, and only forward
26 * (ids sort by time).
27 */
28export function readNow(moment: ReadMoment): string | null {
29 if (!moment.visible || !moment.onScreen || !moment.newestId) return null;
30 if (moment.lastRead !== null && moment.newestId <= moment.lastRead) return null;
31 return moment.newestId;
32}
33
34type Shown = Pick<ChatMessage, "id" | "answer" | "deleted_at"> & { pending?: boolean };
35
36/**
37 * The newest message there to read: saved (not one still being sent) and
38 * finished. An answer still being written is read once it is done, so the
39 * mark lands on the final message, not before it exists.
40 */
41export function newestFinished(messages: readonly Shown[]): string | null {
42 let newest: string | null = null;
43 for (const m of messages) {
44 if (m.pending || m.deleted_at) continue;
45 if (m.answer?.state === "thinking" || m.answer?.state === "streaming") continue;
46 if (newest === null || m.id > newest) newest = m.id;
47 }
48 return newest;
49}
50
51// ── Conversations shown on this page ─────────────────────────────────────
52
53const shown = new Map<string, number>();
54
55/** This page shows `channelId`'s messages until the returned function is called. Counted: two surfaces may show the same one. */
56export function showConversation(channelId: string): () => void {
57 shown.set(channelId, (shown.get(channelId) ?? 0) + 1);
58 let done = false;
59 return () => {
60 if (done) return;
61 done = true;
62 const left = (shown.get(channelId) ?? 1) - 1;
63 if (left > 0) shown.set(channelId, left);
64 else shown.delete(channelId);
65 };
66}
67
68/** The conversations this page shows right now, besides the one open in Chat. */
69export function shownConversations(): ReadonlySet<string> {
70 return new Set(shown.keys());
71}