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.

g1t.sh
183 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Chat has sounds. A short warm cue plays for a message in a conversation you have open but aren't looking at, a direct message, a mention of you and an agent finishing something for you, with a soft tick for sending that is off until you turn it on; never for your own messages, a muted conversation, a conversation you are looking at in a window that has the front, or while you have paused notifications, and never twice for one message or more than once in a second and a half. The cues are made by the browser itself in two sets, Soft and Bright, so nothing is downloaded; the Sounds and notifications section of your notification settings turns them on and off, picks the set and the volume, plays each one, and offers desktop notifications for when the window is behind; Do not disturb is the one pause everyone sees, set from the chat sidebar's bell, the settings, or your avatar menu, now with a two-hour choice, and the top bar's bell shows a dot while it's on. Your choices are kept with your account. The sound layer sits behind one small interface so the desktop app can play them natively later. The chat and notifications guides say the rules.1/**
2 * When chat makes a sound, apart from the browser: which cue a message or
3 * a notification earns, given who you are, what you are looking at, what
4 * you muted and whether you are not to be disturbed; and how close
5 * together two sounds may come. Pure, so every rule is tested on its own
6 * (chat-sounds.test.ts); lib/sound-events.ts runs it on each event with
7 * the page's real state, and lib/sounds.ts plays the result.
8 *
9 * The rules, as the chat guide says them ("Sounds and do not disturb"):
10 *
11 * - `mention` for a message that @mentions you, `direct` for a direct
12 * message, `message` for a message in a conversation you have open while
13 * the window is not in front or you are in another conversation, and
14 * `agent_done` when an agent posts a card saying it finished something.
15 * - Never for your own messages, never in a muted conversation, never
16 * while Do not disturb is on, and never for the conversation you are
17 * looking at in a window that is in front: what is on screen is silent.
18 * - Agents' messages count like people's.
19 * - At most one sound every 1.5 seconds, and never twice for one message,
20 * however many ways it arrives (its conversation's socket and your feed).
21 */
22import type { ChatMessage, FeedNotification } from "@g1t/contracts";
23import { type SoundCue, type SoundSettings, cueOn } from "@g1t/contracts/sounds";
24
25/** What the tab can see of itself: shown, in front, and which conversation it has open. */
26export type FocusState = {
27 /** `document.visibilityState === "visible"`. */
28 visible: boolean;
29 /** `document.hasFocus()`: the window is in front and this tab has the keyboard. */
30 focused: boolean;
31 /** The conversation on screen, by channel id; null on any other page. */
32 viewingChannel: string | null;
33};
34
35export type SoundContext = {
36 me: { id: string; username: string } | null;
37 focus: FocusState;
38 /** Conversations muted in the sidebar, by channel id. */
39 muted: ReadonlySet<string>;
40 /** Do not disturb holds (the person's presence, `dnd_until`). */
41 dnd: boolean;
42 settings: SoundSettings;
43};
44
45/**
46 * Something that arrived: a message over the open conversation's socket,
47 * or a notification over the feed (which only carries what is for you: a
48 * DM, a mention, a reply in your thread, an agent waiting on you).
49 */
50export type Arrival =
51 | { source: "chat"; message: ChatMessage; channelKind: "channel" | "dm" }
52 | { source: "feed"; notification: FeedNotification };
53
54/** No two sounds closer than this: a burst of messages is one sound. */
55export const SOUND_GAP_MS = 1_500;
56/** The same message is not sounded again within this, whichever way it arrives. */
57export const SAME_MESSAGE_MS = 10_000;
58
59/** `@you` in a message's text: whole handles only, so `@you-and-me` is not you, and not inside code. */
60export function mentionsMe(body: string, username: string): boolean {
61 const name = username.trim().toLowerCase();
62 if (!name) return false;
63 const outsideCode = String(body ?? "").replace(/```[\s\S]*?(```|$)/g, " ").replace(/`[^`\n]*`/g, " ");
64 const pattern = new RegExp(`(^|[^a-z0-9_.@-])@${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![a-z0-9_/-])`, "i");
65 return pattern.test(outsideCode);
66}
67
68/** States a card shows when the agent's work is done, as agents' cards word them. */
69const FINISHED = /^(done|finished|complete|completed|filed|merged|shipped|ready|passed)\b/i;
70
71/** Whether a message is an agent's card saying it finished something. */
72export function finishedCard(message: Pick<ChatMessage, "kind" | "author" | "card">): boolean {
73 if (message.kind !== "card" || message.author.kind !== "agent" || !message.card) return false;
74 return FINISHED.test(message.card.state ?? "") || FINISHED.test(message.card.detail ?? "");
75}
76
77/** What a feed notification's kind sounds like; null for what has no sound (inbox items have the bell). */
78export function cueForNotificationKind(kind: FeedNotification["kind"]): SoundCue | null {
79 switch (kind) {
80 case "dm":
81 return "direct";
82 case "mention":
83 case "approval":
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r984 case "security":
Chat has sounds. A short warm cue plays for a message in a conversation you have open but aren't looking at, a direct message, a mention of you and an agent finishing something for you, with a soft tick for sending that is off until you turn it on; never for your own messages, a muted conversation, a conversation you are looking at in a window that has the front, or while you have paused notifications, and never twice for one message or more than once in a second and a half. The cues are made by the browser itself in two sets, Soft and Bright, so nothing is downloaded; the Sounds and notifications section of your notification settings turns them on and off, picks the set and the volume, plays each one, and offers desktop notifications for when the window is behind; Do not disturb is the one pause everyone sees, set from the chat sidebar's bell, the settings, or your avatar menu, now with a two-hour choice, and the top bar's bell shows a dot while it's on. Your choices are kept with your account. The sound layer sits behind one small interface so the desktop app can play them natively later. The chat and notifications guides say the rules.85 return "mention";
86 case "thread_reply":
87 return "message";
88 case "agent_waiting":
89 return "agent_done";
90 default:
91 return null;
92 }
93}
94
95/** The conversation an arrival is in, by channel id; null for an inbox notification. */
96export function channelOf(arrival: Arrival): string | null {
97 return arrival.source === "chat" ? arrival.message.channel_id : (arrival.notification.channel_id ?? null);
98}
99
100/** Whether `me` wrote it. */
101function mine(arrival: Arrival, me: SoundContext["me"]): boolean {
102 if (!me) return false;
103 const who = arrival.source === "chat" ? arrival.message.author : arrival.notification.actor;
104 return who.kind === "user" && who.id === me.id;
105}
106
107/** Whether the person is looking at the conversation right now: on screen, in a window in front. */
108export function lookingAt(channelId: string | null, focus: FocusState): boolean {
109 return channelId !== null && focus.visible && focus.focused && focus.viewingChannel === channelId;
110}
111
112/** The cue an arrival earns, or null for silence. Every rule above, in order. */
113export function cueFor(arrival: Arrival, ctx: SoundContext): SoundCue | null {
114 if (!ctx.settings.sounds_enabled || ctx.dnd) return null;
115 if (mine(arrival, ctx.me)) return null;
116 const channel = channelOf(arrival);
117 if (channel && ctx.muted.has(channel)) return null;
118 if (lookingAt(channel, ctx.focus)) return null;
119 let cue: SoundCue | null;
120 if (arrival.source === "chat") {
121 const { message } = arrival;
122 if (message.deleted_at) return null;
123 if (finishedCard(message)) cue = "agent_done";
124 else if (ctx.me && mentionsMe(message.body, ctx.me.username)) cue = "mention";
125 else cue = arrival.channelKind === "dm" ? "direct" : "message";
126 } else {
127 cue = cueForNotificationKind(arrival.notification.kind);
128 }
129 return cue && cueOn(ctx.settings, cue) ? cue : null;
130}
131
132/** What identifies an arrival, so the same message sounds once: its message id. */
133export function arrivalKey(arrival: Arrival): string {
134 return arrival.source === "chat" ? `msg:${arrival.message.id}` : `msg:${arrival.notification.id}`;
135}
136
137/**
138 * Keeps sounds apart: one every `SOUND_GAP_MS` at most, and the same key
139 * (a message id) once in `SAME_MESSAGE_MS`. `allow` says whether to play
140 * now, and remembers it if so.
141 */
142export class SoundGate {
143 private lastAt = Number.NEGATIVE_INFINITY;
144 private readonly seen = new Map<string, number>();
145 private readonly gapMs: number;
146 private readonly sameMs: number;
147
148 constructor(gapMs = SOUND_GAP_MS, sameMs = SAME_MESSAGE_MS) {
149 this.gapMs = gapMs;
150 this.sameMs = sameMs;
151 }
152
153 allow(key: string | null, now: number): boolean {
154 for (const [k, at] of this.seen) if (now - at > this.sameMs) this.seen.delete(k);
155 if (key !== null && this.seen.has(key)) return false;
156 if (key !== null) this.seen.set(key, now);
157 if (now - this.lastAt < this.gapMs) return false;
158 this.lastAt = now;
159 return true;
160 }
161}
162
163/**
164 * Whether an open tab shows a system notification for a feed notification:
165 * the person turned desktop notifications on and the browser allows them,
166 * the window is not in front, it is something for them that the feed would
167 * toast, and this browser does not already get pushes (which show the same
168 * notification once no tab is in front).
169 */
170export function wantsDesktopToast(input: {
171 notification: Pick<FeedNotification, "kind">;
172 toast: boolean;
173 ctx: Pick<SoundContext, "focus" | "dnd" | "settings">;
174 permission: "default" | "granted" | "denied";
175 /** This browser is subscribed to pushes (`pushChoice() === "on"`). */
176 pushOn: boolean;
177}): boolean {
178 const { ctx } = input;
179 if (!ctx.settings.desktop_toasts || input.permission !== "granted" || input.pushOn) return false;
180 if (ctx.dnd || !input.toast) return false;
181 if (ctx.focus.visible && ctx.focus.focused) return false;
182 return cueForNotificationKind(input.notification.kind) !== null;
183}

This file's history is long; its oldest lines are credited to the oldest commit read.