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.
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 1 | import { BellRing, Send } from "lucide-react"; |
| 2 | import { useEffect, useState } from "react"; | |
| 3 | ||
| 4 | import type { Membership, NotifyLevel, NotifyPreferences, NotifyPreferencesChange, NotifyStatus } from "@g1t/contracts"; | |
| 5 | ||
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 6 | import { Button } from "../ui/button"; |
| 7 | import { Card } from "../ui/card"; | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 8 | import { RadioGroup, RadioOption } from "../ui/radio-group"; |
| 9 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"; | |
| 10 | import { Switch } from "../ui/switch"; | |
| 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. | 11 | import { desktopBridge, disablePush, enablePush, notifyStatus, permission, pushChoice, pushSupported, savePreferences, sendTest } from "../../lib/notify-client"; |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 12 | |
| 13 | const LEVELS: { level: NotifyLevel; label: string; detail: string }[] = [ | |
| g1t is described as the product it is in alpha, and its shell takes the new shape: the README, the docs home and docs/PLAN.md label every feature Live, Preview or Coming; a floating dock with the g1t mark, Today, Chat, Notifications, Agents, Code, Artifacts, your pinned apps and the Apps launcher, with People, Workspace and the account menu (now holding help) at its foot; the workspace's logo and switcher heading a flat sidebar that folds away with Ctrl B, or leading the header's breadcrumbs where there is none; the page in a rounded panel with a full-width header; Today as the front page, with first-time acceptance of agents' pull requests, what needs you and what's at stake, spend today and one item to start with, every number from a service; Notifications in place of the Inbox at /notifications; a bottom bar and More sheet on phones; and sign-in, sign-up, two-factor, reset, invites and choosing a workspace on standalone pages with no app around them. | 14 | { level: "all", label: "Everything", detail: "Every message in conversations you're in, and everything in your notifications." }, |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 15 | { |
| 16 | level: "dms_mentions", | |
| 17 | label: "Direct messages, mentions and replies", | |
| 18 | detail: "Messages to you, @mentions, replies in your threads, and agents or reviews waiting on you.", | |
| 19 | }, | |
| 20 | { level: "none", label: "Nothing", detail: "No pop-ups or browser notifications. Counts still update." }, | |
| 21 | ]; | |
| 22 | ||
| 23 | type Device = "on" | "off" | "blocked" | "unsupported" | "unavailable" | "desktop"; | |
| 24 | ||
| 25 | function deviceState(status: NotifyStatus | null): Device { | |
| 26 | if (desktopBridge()) return "desktop"; | |
| 27 | if (!pushSupported()) return "unsupported"; | |
| 28 | if (status && !status.vapid_public_key) return "unavailable"; | |
| 29 | if (permission() === "denied") return "blocked"; | |
| 30 | return permission() === "granted" && status?.subscribed && pushChoice() !== "off" ? "on" : "off"; | |
| 31 | } | |
| 32 | ||
| 33 | const DEVICE_LINE: Record<Device, string> = { | |
| 34 | on: "On. This browser shows a notification when you're away from g1t.", | |
| 35 | off: "Off. You'll only see notifications while g1t is open.", | |
| 36 | blocked: "Blocked by your browser. Allow notifications for this site in its settings, then come back.", | |
| 37 | unsupported: "This browser can't show notifications.", | |
| 38 | unavailable: "Browser notifications aren't set up on this server.", | |
| 39 | desktop: "The desktop app shows notifications itself.", | |
| 40 | }; | |
| 41 | ||
| 42 | /** | |
| 43 | * Settings → Notifications, the live part: what pops up and is pushed, | |
| 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. | 44 | * this browser's notifications, and a test. Kept by the notify service |
| 45 | * (services/notify). Sounds are the section above (sounds.tsx). | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 46 | */ |
| 47 | export function LiveNotificationSettings({ workspaces, initial = null }: { workspaces: Membership[]; initial?: NotifyStatus | null }) { | |
| 48 | const [status, setStatus] = useState<NotifyStatus | null>(initial); | |
| 49 | const [prefs, setPrefs] = useState<NotifyPreferences | null>(initial?.preferences ?? null); | |
| 50 | const [device, setDevice] = useState<Device>("off"); | |
| 51 | const [busy, setBusy] = useState<string | null>(null); | |
| 52 | const [said, setSaid] = useState<string | null>(null); | |
| 53 | const [failed, setFailed] = useState(false); | |
| 54 | ||
| 55 | useEffect(() => { | |
| 56 | if (initial) { | |
| 57 | setDevice(deviceState(initial)); | |
| 58 | return; | |
| 59 | } | |
| 60 | notifyStatus() | |
| 61 | .then((s) => { | |
| 62 | setStatus(s); | |
| 63 | setPrefs(s?.preferences ?? null); | |
| 64 | setDevice(deviceState(s)); | |
| 65 | if (!s) setFailed(true); | |
| 66 | }) | |
| 67 | .catch(() => setFailed(true)); | |
| 68 | }, [initial]); | |
| 69 | ||
| 70 | const save = async (change: NotifyPreferencesChange) => { | |
| 71 | if (prefs) { | |
| 72 | const workspaces = { ...prefs.workspaces }; | |
| 73 | for (const [slug, level] of Object.entries(change.workspaces ?? {})) { | |
| 74 | if (level) workspaces[slug] = level; | |
| 75 | else delete workspaces[slug]; | |
| 76 | } | |
| 77 | setPrefs({ level: change.level ?? prefs.level, workspaces }); | |
| 78 | } | |
| 79 | setBusy("prefs"); | |
| 80 | const saved = await savePreferences(change).catch(() => null); | |
| 81 | setBusy(null); | |
| 82 | if (saved) { | |
| 83 | setPrefs(saved); | |
| 84 | setSaid("Saved."); | |
| 85 | } else setSaid("That didn't save. Try again in a moment."); | |
| 86 | }; | |
| 87 | ||
| 88 | const toggleDevice = async (on: boolean) => { | |
| 89 | setBusy("device"); | |
| 90 | if (on) { | |
| 91 | const result = await enablePush(); | |
| 92 | setDevice(result === "on" ? "on" : result === "denied" ? (permission() === "denied" ? "blocked" : "off") : deviceState(status)); | |
| 93 | if (result === "failed") setSaid("This browser couldn't be set up. Try again in a moment."); | |
| 94 | } else { | |
| 95 | await disablePush().catch(() => {}); | |
| 96 | setDevice("off"); | |
| 97 | } | |
| 98 | setBusy(null); | |
| 99 | }; | |
| 100 | ||
| 101 | const test = async () => { | |
| 102 | setBusy("test"); | |
| 103 | const sent = await sendTest().catch(() => null); | |
| 104 | setBusy(null); | |
| 105 | if (!sent) setSaid("The test didn't send. Try again in a moment."); | |
| 106 | else if (sent.pushed > 0) setSaid(`Sent. Also pushed to ${sent.pushed === 1 ? "1 browser" : `${sent.pushed} browsers`}.`); | |
| 107 | else setSaid("Sent. It shows here; turn on browser notifications to get it when you're away."); | |
| 108 | }; | |
| 109 | ||
| 110 | if (failed) { | |
| 111 | return ( | |
| 112 | <section aria-labelledby="live-heading"> | |
| 113 | <h2 id="live-heading" className="text-sm font-semibold"> | |
| 114 | Pop-ups and browser notifications | |
| 115 | </h2> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 116 | <Card asChild tone="plain" radius="lg" className="mt-3 px-4 py-6 text-sm text-muted"> |
| 117 | <p>These settings could not be loaded. Try again in a moment.</p> | |
| 118 | </Card> | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 119 | </section> |
| 120 | ); | |
| 121 | } | |
| 122 | ||
| 123 | const deviceOn = device === "on"; | |
| 124 | const deviceLocked = device === "unsupported" || device === "unavailable" || device === "blocked" || device === "desktop"; | |
| 125 | ||
| 126 | return ( | |
| 127 | <section aria-labelledby="live-heading" className="space-y-6"> | |
| 128 | <div> | |
| 129 | <h2 id="live-heading" className="text-sm font-semibold"> | |
| 130 | Pop-ups and browser notifications | |
| 131 | </h2> | |
| 132 | <p className="mt-1 text-sm text-muted">What pops up while g1t is open, and what reaches you when it isn't.</p> | |
| 133 | </div> | |
| 134 | ||
| 135 | <div> | |
| 136 | <h3 className="text-[0.8125rem] font-medium text-fg">Notify me about</h3> | |
| 137 | <RadioGroup | |
| 138 | className="mt-3" | |
| 139 | value={prefs?.level ?? "dms_mentions"} | |
| 140 | disabled={!prefs} | |
| 141 | onValueChange={(value) => void save({ level: value as NotifyLevel })} | |
| 142 | aria-label="Notify me about" | |
| 143 | > | |
| 144 | {LEVELS.map((choice) => ( | |
| 145 | <RadioOption key={choice.level} value={choice.level} label={choice.label} description={choice.detail} /> | |
| 146 | ))} | |
| 147 | </RadioGroup> | |
| 148 | </div> | |
| 149 | ||
| 150 | {workspaces.length > 1 && prefs && ( | |
| 151 | <div> | |
| 152 | <h3 className="text-[0.8125rem] font-medium text-fg">For a workspace</h3> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 153 | <Card asChild tone="plain" radius="lg" divided className="mt-3"> |
| 154 | <ul> | |
| 155 | {workspaces.map((membership) => ( | |
| 156 | <li key={membership.slug} className="flex items-center gap-3 px-3.5 py-2.5"> | |
| 157 | <span className="min-w-0 grow truncate text-sm">{membership.name || membership.slug}</span> | |
| 158 | <Select | |
| 159 | value={prefs.workspaces[membership.slug] ?? "default"} | |
| 160 | onValueChange={(value) => void save({ workspaces: { [membership.slug]: value === "default" ? null : (value as NotifyLevel) } })} | |
| 161 | > | |
| 162 | <SelectTrigger size="sm" className="w-44" aria-label={`Notifications for ${membership.name || membership.slug}`}> | |
| 163 | <SelectValue /> | |
| 164 | </SelectTrigger> | |
| 165 | <SelectContent> | |
| 166 | <SelectItem value="default">As above</SelectItem> | |
| 167 | <SelectItem value="all">Everything</SelectItem> | |
| 168 | <SelectItem value="dms_mentions">DMs and mentions</SelectItem> | |
| 169 | <SelectItem value="none">Nothing</SelectItem> | |
| 170 | </SelectContent> | |
| 171 | </Select> | |
| 172 | </li> | |
| 173 | ))} | |
| 174 | </ul> | |
| 175 | </Card> | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 176 | </div> |
| 177 | )} | |
| 178 | ||
| 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. | 179 | <Card> |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 180 | <div className="flex items-start gap-4 p-4"> |
| 181 | <span className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg bg-accent/12 text-accent"> | |
| 182 | <BellRing size={16} /> | |
| 183 | </span> | |
| 184 | <div className="min-w-0 grow"> | |
| 185 | <p className="text-sm font-medium text-fg">Browser notifications</p> | |
| 186 | <p className="mt-0.5 text-xs text-muted" role="status"> | |
| 187 | {DEVICE_LINE[device]} | |
| 188 | </p> | |
| 189 | </div> | |
| 190 | <Switch | |
| 191 | checked={deviceOn} | |
| 192 | disabled={deviceLocked || busy === "device" || !status} | |
| 193 | onCheckedChange={(on) => void toggleDevice(on)} | |
| 194 | aria-label="Browser notifications" | |
| 195 | /> | |
| 196 | </div> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 197 | </Card> |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 198 | |
| 199 | <div className="flex flex-wrap items-center gap-3"> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 200 | <Button type="button" variant="outline" onClick={() => void test()} disabled={busy === "test" || !status}> |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 201 | <Send size={14} /> |
| 202 | {busy === "test" ? "Sending…" : "Send a test notification"} | |
| 203 | </Button> | |
| 204 | {said && <p className="text-xs text-muted">{said}</p>} | |
| 205 | </div> | |
| 206 | </section> | |
| 207 | ); | |
| 208 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.