Skip to content
238 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.

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)1import { BellRing, Send } from "lucide-react";
2import { useEffect, useState } from "react";
3
4import 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.6import { Button } from "../ui/button";
7import { 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)8import { RadioGroup, RadioOption } from "../ui/radio-group";
9import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select";
10import { Switch } from "../ui/switch";
11import {
12 desktopBridge,
13 disablePush,
14 enablePush,
15 notifyStatus,
16 permission,
17 pushChoice,
18 pushSupported,
19 savePreferences,
20 sendTest,
21 setSound,
22 soundOn,
23 chime,
24} from "../../lib/notify-client";
25
26const 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.27 { 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)28 {
29 level: "dms_mentions",
30 label: "Direct messages, mentions and replies",
31 detail: "Messages to you, @mentions, replies in your threads, and agents or reviews waiting on you.",
32 },
33 { level: "none", label: "Nothing", detail: "No pop-ups or browser notifications. Counts still update." },
34];
35
36type Device = "on" | "off" | "blocked" | "unsupported" | "unavailable" | "desktop";
37
38function deviceState(status: NotifyStatus | null): Device {
39 if (desktopBridge()) return "desktop";
40 if (!pushSupported()) return "unsupported";
41 if (status && !status.vapid_public_key) return "unavailable";
42 if (permission() === "denied") return "blocked";
43 return permission() === "granted" && status?.subscribed && pushChoice() !== "off" ? "on" : "off";
44}
45
46const DEVICE_LINE: Record<Device, string> = {
47 on: "On. This browser shows a notification when you're away from g1t.",
48 off: "Off. You'll only see notifications while g1t is open.",
49 blocked: "Blocked by your browser. Allow notifications for this site in its settings, then come back.",
50 unsupported: "This browser can't show notifications.",
51 unavailable: "Browser notifications aren't set up on this server.",
52 desktop: "The desktop app shows notifications itself.",
53};
54
55/**
56 * Settings → Notifications, the live part: what pops up and is pushed,
57 * this browser's notifications, the sound, and a test. Kept by the notify
58 * service (services/notify); the sound is this browser's own.
59 */
60export function LiveNotificationSettings({ workspaces, initial = null }: { workspaces: Membership[]; initial?: NotifyStatus | null }) {
61 const [status, setStatus] = useState<NotifyStatus | null>(initial);
62 const [prefs, setPrefs] = useState<NotifyPreferences | null>(initial?.preferences ?? null);
63 const [device, setDevice] = useState<Device>("off");
64 const [sound, setSoundState] = useState(false);
65 const [busy, setBusy] = useState<string | null>(null);
66 const [said, setSaid] = useState<string | null>(null);
67 const [failed, setFailed] = useState(false);
68
69 useEffect(() => {
70 setSoundState(soundOn());
71 if (initial) {
72 setDevice(deviceState(initial));
73 return;
74 }
75 notifyStatus()
76 .then((s) => {
77 setStatus(s);
78 setPrefs(s?.preferences ?? null);
79 setDevice(deviceState(s));
80 if (!s) setFailed(true);
81 })
82 .catch(() => setFailed(true));
83 }, [initial]);
84
85 const save = async (change: NotifyPreferencesChange) => {
86 if (prefs) {
87 const workspaces = { ...prefs.workspaces };
88 for (const [slug, level] of Object.entries(change.workspaces ?? {})) {
89 if (level) workspaces[slug] = level;
90 else delete workspaces[slug];
91 }
92 setPrefs({ level: change.level ?? prefs.level, workspaces });
93 }
94 setBusy("prefs");
95 const saved = await savePreferences(change).catch(() => null);
96 setBusy(null);
97 if (saved) {
98 setPrefs(saved);
99 setSaid("Saved.");
100 } else setSaid("That didn't save. Try again in a moment.");
101 };
102
103 const toggleDevice = async (on: boolean) => {
104 setBusy("device");
105 if (on) {
106 const result = await enablePush();
107 setDevice(result === "on" ? "on" : result === "denied" ? (permission() === "denied" ? "blocked" : "off") : deviceState(status));
108 if (result === "failed") setSaid("This browser couldn't be set up. Try again in a moment.");
109 } else {
110 await disablePush().catch(() => {});
111 setDevice("off");
112 }
113 setBusy(null);
114 };
115
116 const test = async () => {
117 setBusy("test");
118 const sent = await sendTest().catch(() => null);
119 setBusy(null);
120 if (!sent) setSaid("The test didn't send. Try again in a moment.");
121 else if (sent.pushed > 0) setSaid(`Sent. Also pushed to ${sent.pushed === 1 ? "1 browser" : `${sent.pushed} browsers`}.`);
122 else setSaid("Sent. It shows here; turn on browser notifications to get it when you're away.");
123 };
124
125 if (failed) {
126 return (
127 <section aria-labelledby="live-heading">
128 <h2 id="live-heading" className="text-sm font-semibold">
129 Pop-ups and browser notifications
130 </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.131 <Card asChild tone="plain" radius="lg" className="mt-3 px-4 py-6 text-sm text-muted">
132 <p>These settings could not be loaded. Try again in a moment.</p>
133 </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)134 </section>
135 );
136 }
137
138 const deviceOn = device === "on";
139 const deviceLocked = device === "unsupported" || device === "unavailable" || device === "blocked" || device === "desktop";
140
141 return (
142 <section aria-labelledby="live-heading" className="space-y-6">
143 <div>
144 <h2 id="live-heading" className="text-sm font-semibold">
145 Pop-ups and browser notifications
146 </h2>
147 <p className="mt-1 text-sm text-muted">What pops up while g1t is open, and what reaches you when it isn't.</p>
148 </div>
149
150 <div>
151 <h3 className="text-[0.8125rem] font-medium text-fg">Notify me about</h3>
152 <RadioGroup
153 className="mt-3"
154 value={prefs?.level ?? "dms_mentions"}
155 disabled={!prefs}
156 onValueChange={(value) => void save({ level: value as NotifyLevel })}
157 aria-label="Notify me about"
158 >
159 {LEVELS.map((choice) => (
160 <RadioOption key={choice.level} value={choice.level} label={choice.label} description={choice.detail} />
161 ))}
162 </RadioGroup>
163 </div>
164
165 {workspaces.length > 1 && prefs && (
166 <div>
167 <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.168 <Card asChild tone="plain" radius="lg" divided className="mt-3">
169 <ul>
170 {workspaces.map((membership) => (
171 <li key={membership.slug} className="flex items-center gap-3 px-3.5 py-2.5">
172 <span className="min-w-0 grow truncate text-sm">{membership.name || membership.slug}</span>
173 <Select
174 value={prefs.workspaces[membership.slug] ?? "default"}
175 onValueChange={(value) => void save({ workspaces: { [membership.slug]: value === "default" ? null : (value as NotifyLevel) } })}
176 >
177 <SelectTrigger size="sm" className="w-44" aria-label={`Notifications for ${membership.name || membership.slug}`}>
178 <SelectValue />
179 </SelectTrigger>
180 <SelectContent>
181 <SelectItem value="default">As above</SelectItem>
182 <SelectItem value="all">Everything</SelectItem>
183 <SelectItem value="dms_mentions">DMs and mentions</SelectItem>
184 <SelectItem value="none">Nothing</SelectItem>
185 </SelectContent>
186 </Select>
187 </li>
188 ))}
189 </ul>
190 </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)191 </div>
192 )}
193
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.194 <Card divided>
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)195 <div className="flex items-start gap-4 p-4">
196 <span className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg bg-accent/12 text-accent">
197 <BellRing size={16} />
198 </span>
199 <div className="min-w-0 grow">
200 <p className="text-sm font-medium text-fg">Browser notifications</p>
201 <p className="mt-0.5 text-xs text-muted" role="status">
202 {DEVICE_LINE[device]}
203 </p>
204 </div>
205 <Switch
206 checked={deviceOn}
207 disabled={deviceLocked || busy === "device" || !status}
208 onCheckedChange={(on) => void toggleDevice(on)}
209 aria-label="Browser notifications"
210 />
211 </div>
212 <div className="flex items-start gap-4 p-4">
213 <div className="min-w-0 grow pl-12">
214 <p className="text-sm font-medium text-fg">Sound</p>
215 <p className="mt-0.5 text-xs text-muted">A soft chime with each pop-up, in this browser.</p>
216 </div>
217 <Switch
218 checked={sound}
219 onCheckedChange={(on) => {
220 setSound(on);
221 setSoundState(on);
222 if (on) chime();
223 }}
224 aria-label="Sound"
225 />
226 </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.227 </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)228
229 <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.230 <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)231 <Send size={14} />
232 {busy === "test" ? "Sending…" : "Send a test notification"}
233 </Button>
234 {said && <p className="text-xs text-muted">{said}</p>}
235 </div>
236 </section>
237 );
238}

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