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
445 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 and workspace agents: channels, DMs and named agents you talk to1import { Check, Hash, Lock, Plus, Search, SquarePen, X } from "lucide-react";
2import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
3import { useFetcher, useNavigate, useRouteLoaderData } from "react-router";
4
5import type { ChatSidebar, Result } from "@g1t/contracts";
6
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)7import { AgentPill } from "./marks";
8import { AgentAvatar } from "../agent-avatar";
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.9
10import { Avatar } from "../ui/avatar";
11import { Button } from "../ui/button";
12import { Card } from "../ui/card";
Chat and workspace agents: channels, DMs and named agents you talk to13import { Hint } from "../ui/hint";
14import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog";
15import { Switch } from "../ui/switch";
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar16import { channelName, shownHandle, shownName } from "../../lib/chat";
Chat and workspace agents: channels, DMs and named agents you talk to17import type { ChatLayoutData } from "../../routes/workspace/chat/layout";
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)18import { useLiveCounts } from "../../lib/notify-client";
19import { overlayEntries, unlisted } from "../../lib/notify-store";
Chat and workspace agents: channels, DMs and named agents you talk to20
21/** Chat mode's data, while a chat page is open; undefined elsewhere. */
22export function useChatData(): ChatLayoutData | undefined {
23 return useRouteLoaderData("routes/workspace/chat/layout") as ChatLayoutData | undefined;
24}
25
26/** The sidebar's own fetcher: every component that refreshes it shares this one. */
27export const SIDEBAR_FETCHER = "chat-sidebar";
28
29/** The sidebar as last read: refreshed in place, else what the page loaded. */
30export function useChatSidebar(): { sidebar: ChatSidebar | null; refresh: () => void } {
31 const data = useChatData();
32 const fetcher = useFetcher<Result<ChatSidebar>>({ key: SIDEBAR_FETCHER });
33 const slug = data?.slug;
34 const refresh = useCallback(() => {
35 if (slug) fetcher.load(`/${slug}/-/chat/api`);
36 // eslint-disable-next-line react-hooks/exhaustive-deps
37 }, [slug]);
38 const fresh = fetcher.data?.ok ? fetcher.data.value : null;
39 const sidebar = fresh ?? data?.sidebar ?? null;
40 // A conversation read on its page reads as read everywhere at once, until
41 // the service's next answer says so itself.
42 const [read, setRead] = useState<ReadonlySet<string>>(new Set());
43 useEffect(() => {
44 const onRead = (event: Event) => setRead((now) => new Set(now).add(String((event as CustomEvent).detail)));
45 window.addEventListener("g1t:chat-read", onRead);
46 return () => window.removeEventListener("g1t:chat-read", onRead);
47 }, []);
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)48 // Notify: the feed socket's counts (lib/notify-client.ts) over the sidebar as read, live.
49 const live = useLiveCounts(slug);
50 useEffect(() => setRead(new Set()), [sidebar, live]);
51 const shown = useMemo(() => {
52 const counted = sidebar && live ? { ...sidebar, entries: overlayEntries(sidebar.entries, live) } : sidebar;
53 return counted && read.size > 0
54 ? { ...counted, entries: counted.entries.map((e) => (read.has(e.channel.id) ? { ...e, unread: 0, mentions: 0 } : e)) }
55 : counted;
56 }, [sidebar, read, live]);
57 // A conversation the feed counts that the sidebar does not list (a new DM): read the sidebar again.
58 const missing = sidebar ? unlisted(sidebar.entries, live).join(",") : "";
59 useEffect(() => {
60 if (missing) refresh();
61 }, [missing, refresh]);
Chat and workspace agents: channels, DMs and named agents you talk to62 return { sidebar: shown, refresh };
63}
64
65/**
66 * Sends one request to the chat's JSON route and answers with the
67 * service's result; a network failure is a failure like any other. Doing
68 * something that changes the sidebar refreshes it.
69 */
70export function useChatSend(slug: string) {
71 const fetcher = useFetcher<Result<ChatSidebar>>({ key: SIDEBAR_FETCHER });
72 return useCallback(
73 async <T = unknown,>(body: Record<string, unknown>): Promise<Result<T>> => {
74 try {
75 const response = await fetch(`/${slug}/-/chat/api`, {
76 method: "POST",
77 headers: { "content-type": "application/json" },
78 body: JSON.stringify(body),
79 });
80 const result = (await response.json()) as Result<T>;
81 if (result.ok && body.intent !== "post" && body.intent !== "read") fetcher.load(`/${slug}/-/chat/api`);
82 return result;
83 } catch {
84 return { ok: false, error: { code: "conflict", message: "Couldn't reach g1t. Check your connection and try again." } };
85 }
86 },
87 // eslint-disable-next-line react-hooks/exhaustive-deps
88 [slug],
89 );
90}
91
Chat controls, public profiles, shadcn selects, and no Docs tab in a project92/** Why someone may not make a channel: the workspace keeps it to its owners. */
93export const OWNERS_ONLY_CHANNELS = "Only workspace owners can create channels here.";
94
95/**
96 * What the viewer may make, from the workspace's chat settings (Settings,
97 * Chat). Until the sidebar arrives, everything: the chat service checks.
98 */
99export function useChannelCreation(): { public: boolean; private: boolean; any: boolean } {
100 const { sidebar } = useChatSidebar();
101 const open = sidebar?.can?.create_public_channels ?? true;
102 const closed = sidebar?.can?.create_private_channels ?? true;
103 return { public: open, private: closed, any: open || closed };
104}
105
106/**
107 * Making a channel: its name, what it is for, and whether it is private.
108 * Someone the workspace's settings do not let make one sees the button
109 * turned off, with why; someone who may make only one kind gets that kind.
110 */
Chat and workspace agents: channels, DMs and named agents you talk to111export function CreateChannelButton({ slug, variant = "icon" }: { slug: string; variant?: "icon" | "button" }) {
112 const [open, setOpen] = useState(false);
113 const [name, setName] = useState("");
114 const [topic, setTopic] = useState("");
115 const [secret, setSecret] = useState(false);
116 const [error, setError] = useState<string | null>(null);
117 const [busy, setBusy] = useState(false);
118 const send = useChatSend(slug);
119 const navigate = useNavigate();
Chat controls, public profiles, shadcn selects, and no Docs tab in a project120 const may = useChannelCreation();
121 // Only one kind allowed: that kind, and the switch says why it is fixed.
122 const isPrivate = !may.public ? true : !may.private ? false : secret;
Chat and workspace agents: channels, DMs and named agents you talk to123 const clean = channelName(name);
124 const submit = async () => {
125 if (!clean) return setError("Give the channel a name.");
126 setBusy(true);
127 setError(null);
Chat controls, public profiles, shadcn selects, and no Docs tab in a project128 const made = await send<{ to: string }>({ intent: "create_channel", name: clean, topic, private: isPrivate });
Chat and workspace agents: channels, DMs and named agents you talk to129 setBusy(false);
130 if (!made.ok) return setError(made.error.message);
131 setOpen(false);
132 setName("");
133 setTopic("");
134 navigate(made.value.to);
135 };
Chat controls, public profiles, shadcn selects, and no Docs tab in a project136 if (!may.any) {
137 // Shown off, not hidden, so nobody wonders where it went.
138 return (
139 <Hint label={OWNERS_ONLY_CHANNELS} disabled>
140 {variant === "icon" ? (
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.141 <Button type="button" aria-label="Create a channel" disabled variant="ghost" size="icon-xs" className="text-faint opacity-40">
Chat controls, public profiles, shadcn selects, and no Docs tab in a project142 <Plus size={14} />
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.143 </Button>
Chat controls, public profiles, shadcn selects, and no Docs tab in a project144 ) : (
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.145 <Button type="button" disabled variant="outline" className="opacity-50">
Chat controls, public profiles, shadcn selects, and no Docs tab in a project146 <Lock size={14} />
147 Create a channel
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.148 </Button>
Chat controls, public profiles, shadcn selects, and no Docs tab in a project149 )}
150 </Hint>
151 );
152 }
Chat and workspace agents: channels, DMs and named agents you talk to153 return (
154 <>
155 {variant === "icon" ? (
156 <Hint label="Create a channel">
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.157 <Button type="button" aria-label="Create a channel" onClick={() => setOpen(true)} variant="ghost" size="icon-xs" className="text-faint">
Chat and workspace agents: channels, DMs and named agents you talk to158 <Plus size={14} />
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.159 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to160 </Hint>
161 ) : (
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.162 <Button type="button" onClick={() => setOpen(true)} variant="outline">
Chat and workspace agents: channels, DMs and named agents you talk to163 <Hash size={15} />
164 Create a channel
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.165 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to166 )}
167 <Dialog open={open} onOpenChange={setOpen}>
168 <DialogContent>
169 <DialogHeader>
170 <DialogTitle>Create a channel</DialogTitle>
171 <DialogDescription>A place for one topic: a project, a team, a launch. Invite people and agents once it's made.</DialogDescription>
172 </DialogHeader>
173 <form
174 className="grid gap-4"
175 onSubmit={(event) => {
176 event.preventDefault();
177 void submit();
178 }}
179 >
180 <label className="grid gap-1.5">
181 <span className="text-sm font-medium text-muted">Name</span>
182 <span className="flex h-10 items-center gap-1.5 rounded-md border border-line bg-bg px-3 focus-within:border-accent-dim">
Chat controls, public profiles, shadcn selects, and no Docs tab in a project183 {isPrivate ? <Lock size={14} className="text-faint" /> : <Hash size={14} className="text-faint" />}
Chat and workspace agents: channels, DMs and named agents you talk to184 <input
185 autoFocus
186 value={name}
187 onChange={(event) => setName(event.target.value)}
188 placeholder="release-train"
189 maxLength={80}
190 autoComplete="off"
191 data-1p-ignore
192 className="min-w-0 grow bg-transparent text-sm outline-none placeholder:text-faint"
193 />
194 </span>
195 {name && clean !== name && <span className="text-xs text-faint">It will be #{clean || "…"}</span>}
196 </label>
197 <label className="grid gap-1.5">
198 <span className="text-sm font-medium text-muted">
199 Topic <span className="font-normal text-faint">(optional)</span>
200 </span>
201 <input
202 value={topic}
203 onChange={(event) => setTopic(event.target.value)}
204 placeholder="What this channel is for"
205 maxLength={250}
206 autoComplete="off"
207 data-1p-ignore
208 className="h-10 rounded-md border border-line bg-bg px-3 text-sm outline-none placeholder:text-faint focus:border-accent-dim"
209 />
210 </label>
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.211 <Card asChild tone="plain" radius="lg" className="flex items-start justify-between gap-4 bg-bg/60 p-3">
212 <label>
213 <span>
214 <span className="flex items-center gap-1.5 text-sm font-medium">
215 {isPrivate ? <Lock size={13} className="text-muted" /> : <Hash size={13} className="text-muted" />}
216 {isPrivate ? "Private" : "Public"}
217 </span>
218 <span className="mt-0.5 block text-xs text-muted">
219 {!may.public
220 ? "Only workspace owners can create public channels here, so this one is private: only people and agents you invite can find and read it."
221 : !may.private
222 ? "Only workspace owners can create private channels here, so this one is public: anyone in the workspace can find, read and join it."
223 : isPrivate
224 ? "Only people and agents you invite can find and read it."
225 : "Anyone in the workspace can find, read and join it. Turn on to keep it to the people and agents you invite."}
226 </span>
Chat controls, public profiles, shadcn selects, and no Docs tab in a project227 </span>
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.228 <Switch checked={isPrivate} onCheckedChange={setSecret} disabled={!may.public || !may.private} aria-label="Private" />
229 </label>
230 </Card>
Chat and workspace agents: channels, DMs and named agents you talk to231 {error && <p className="text-sm text-danger">{error}</p>}
232 <DialogFooter>
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.233 <Button type="button" onClick={() => setOpen(false)} variant="outline">
Chat and workspace agents: channels, DMs and named agents you talk to234 Cancel
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.235 </Button>
236 <Button
Chat and workspace agents: channels, DMs and named agents you talk to237 type="submit"
238 disabled={busy || !clean}
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.239 variant="accent"
Chat and workspace agents: channels, DMs and named agents you talk to240 >
241 {busy ? "Creating…" : "Create channel"}
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.242 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to243 </DialogFooter>
244 </form>
245 </DialogContent>
246 </Dialog>
247 </>
248 );
249}
250
251/** Someone a new message can go to. */
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar252type Pickable = { key: string; kind: "user" | "agent"; name: string; handle: string; display: string; avatar: string | null; role?: string | null };
Chat and workspace agents: channels, DMs and named agents you talk to253
254/**
255 * A new direct message: choose people, agents or both, then open the
256 * conversation between you (the same one each time for the same people).
257 */
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)258export function NewMessageButton({ slug, variant = "icon", children }: { slug: string; variant?: "icon" | "button" | "fab"; children?: ReactNode }) {
Chat and workspace agents: channels, DMs and named agents you talk to259 const [open, setOpen] = useState(false);
260 return (
261 <>
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)262 {variant === "fab" ? (
263 // A phone's list: the round button above the tab bar.
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.264 <Button
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)265 type="button"
266 aria-label="New message"
267 onClick={() => setOpen(true)}
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.268 variant="accent" size="icon"
269 className="size-14 rounded-2xl shadow-lg shadow-black/50 transition-transform active:scale-95"
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)270 >
271 <SquarePen size={22} />
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.272 </Button>
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)273 ) : variant === "icon" ? (
Chat and workspace agents: channels, DMs and named agents you talk to274 <Hint label="New message">
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.275 <Button type="button" aria-label="New message" onClick={() => setOpen(true)} variant="ghost" size="icon-sm" className="text-faint">
Chat and workspace agents: channels, DMs and named agents you talk to276 <SquarePen size={15} />
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.277 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to278 </Hint>
279 ) : (
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.280 <Button type="button" onClick={() => setOpen(true)} variant="outline">
Chat and workspace agents: channels, DMs and named agents you talk to281 <SquarePen size={15} />
282 {children ?? "New message"}
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.283 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to284 )}
285 <Dialog open={open} onOpenChange={setOpen}>
Chat on a phone: the list and its new-message button follow the real top and tab bar heights; the new-message sheet keeps clear of the home indicator without extra padding286 <DialogContent className="max-w-md gap-0 p-0 max-sm:pb-[env(safe-area-inset-bottom)]" showClose={false}>
Chat and workspace agents: channels, DMs and named agents you talk to287 {open && <NewMessage slug={slug} onDone={() => setOpen(false)} />}
288 </DialogContent>
289 </Dialog>
290 </>
291 );
292}
293
294function NewMessage({ slug, onDone }: { slug: string; onDone: () => void }) {
295 const data = useChatData();
296 const [query, setQuery] = useState("");
297 const [chosen, setChosen] = useState<Pickable[]>([]);
298 const [active, setActive] = useState(0);
299 const [error, setError] = useState<string | null>(null);
300 const [busy, setBusy] = useState(false);
301 const input = useRef<HTMLInputElement>(null);
302 const send = useChatSend(slug);
303 const navigate = useNavigate();
304 const everyone = useMemo<Pickable[]>(() => {
305 const me = data?.me.username.toLowerCase();
306 return [
Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how.307 // A personal agent is only in its own DM, opened from the sidebar, never with others.
308 ...(data?.agents ?? []).filter((agent) => agent.scope !== "personal").map((agent) => ({
Chat and workspace agents: channels, DMs and named agents you talk to309 key: `agent:${agent.id}`,
310 kind: "agent" as const,
311 name: agent.handle,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar312 handle: agent.handle,
Chat and workspace agents: channels, DMs and named agents you talk to313 display: agent.display_name,
314 avatar: agent.avatar,
315 role: agent.role,
316 })),
317 ...(data?.people ?? [])
318 .filter((person) => person.name.toLowerCase() !== me)
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar319 .map((person) => ({ key: `user:${person.name}`, kind: "user" as const, name: person.name, handle: shownHandle(person), display: shownName(person), avatar: person.avatar })),
Chat and workspace agents: channels, DMs and named agents you talk to320 ];
321 }, [data]);
322 const q = query.trim().toLowerCase().replace(/^@/, "");
323 const picked = new Set(chosen.map((p) => p.key));
324 const options = everyone.filter(
325 (p) => !picked.has(p.key) && (!q || p.name.toLowerCase().includes(q) || p.display.toLowerCase().includes(q)),
326 );
327 const take = (person: Pickable) => {
328 setChosen((now) => [...now, person]);
329 setQuery("");
330 setActive(0);
331 input.current?.focus();
332 };
333 const go = async () => {
334 if (chosen.length === 0) return;
335 setBusy(true);
336 setError(null);
337 const opened = await send<{ to: string }>({ intent: "dm", members: chosen.map((p) => p.key) });
338 setBusy(false);
339 if (!opened.ok) return setError(opened.error.message);
340 onDone();
341 navigate(opened.value.to);
342 };
343 return (
344 <div className="flex max-h-[min(36rem,calc(100dvh-2rem))] flex-col">
345 <div className="border-b border-line px-5 pt-5 pb-4">
346 <DialogTitle>New message</DialogTitle>
347 <DialogDescription className="mt-1">To one person, a few, or an agent. Agents answer in the conversation.</DialogDescription>
348 <div className="mt-4 flex min-h-10 flex-wrap items-center gap-1.5 rounded-md border border-line bg-bg px-2 py-1.5 focus-within:border-accent-dim">
349 <span className="pl-1 text-sm text-faint">To:</span>
350 {chosen.map((person) => (
351 <span key={person.key} className="flex h-6 items-center gap-1.5 rounded-md bg-raised pr-1 pl-1 text-[0.8125rem]">
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)352 {person.kind === "agent" ? <AgentAvatar agent={{ id: person.key.slice(6), handle: person.name, avatar: person.avatar }} size={16} /> : <Avatar name={person.name} image={person.avatar} size={16} />}
Chat and workspace agents: channels, DMs and named agents you talk to353 {person.display}
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.354 <Button
Chat and workspace agents: channels, DMs and named agents you talk to355 type="button"
356 aria-label={`Remove ${person.display}`}
357 onClick={() => setChosen((now) => now.filter((p) => p.key !== person.key))}
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.358 variant="ghost"
359 size="inline"
360 className="rounded p-0.5 text-faint hover:bg-line"
Chat and workspace agents: channels, DMs and named agents you talk to361 >
362 <X size={12} />
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.363 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to364 </span>
365 ))}
366 <input
367 ref={input}
368 autoFocus
369 value={query}
370 onChange={(event) => {
371 setQuery(event.target.value);
372 setActive(0);
373 }}
374 onKeyDown={(event) => {
375 if (event.key === "ArrowDown" || event.key === "ArrowUp") {
376 event.preventDefault();
377 const count = Math.max(1, options.length);
378 setActive((index) => (index + (event.key === "ArrowDown" ? 1 : count - 1)) % count);
379 } else if (event.key === "Enter") {
380 event.preventDefault();
381 const person = options[active];
382 if (person && q) take(person);
383 else void go();
384 } else if (event.key === "Backspace" && !query && chosen.length) {
385 setChosen((now) => now.slice(0, -1));
386 }
387 }}
388 placeholder={chosen.length ? "" : "Name or @handle"}
389 aria-label="Who to message"
390 autoComplete="off"
391 data-1p-ignore
392 className="h-7 min-w-24 grow bg-transparent px-1 text-sm outline-none placeholder:text-faint"
393 />
394 </div>
395 </div>
396 <ul role="listbox" aria-label="People and agents" className="min-h-0 grow overflow-y-auto p-1.5">
397 {options.length === 0 && (
398 <li className="flex items-center gap-2 px-3 py-6 text-sm text-muted">
399 <Search size={14} />
400 {everyone.length === 0 ? "No one else is in this workspace yet." : "No one matches."}
401 </li>
402 )}
403 {options.map((person, index) => (
404 <li
405 key={person.key}
406 role="option"
407 aria-selected={index === active}
408 onMouseEnter={() => setActive(index)}
409 onClick={() => take(person)}
410 className={`flex cursor-pointer items-center gap-3 rounded-md px-3 py-2 ${index === active ? "bg-raised" : ""}`}
411 >
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)412 {person.kind === "agent" ? <AgentAvatar agent={{ id: person.key.slice(6), handle: person.name, avatar: person.avatar }} size={28} /> : <Avatar name={person.name} image={person.avatar} size={28} />}
Chat and workspace agents: channels, DMs and named agents you talk to413 <span className="min-w-0 grow">
414 <span className="flex items-center gap-1.5 text-sm font-medium">
415 <span className="truncate">{person.display}</span>
416 {person.kind === "agent" && <AgentPill />}
417 </span>
418 <span className="block truncate text-xs text-faint">
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar419 @{person.handle}
Chat and workspace agents: channels, DMs and named agents you talk to420 {person.role ? ` · ${person.role}` : ""}
421 </span>
422 </span>
423 {index === active && <Check size={14} className="text-faint" />}
424 </li>
425 ))}
426 </ul>
427 <div className="flex items-center justify-between gap-3 border-t border-line px-5 py-3">
428 <span className="min-w-0 truncate text-sm text-danger">{error}</span>
429 <div className="flex shrink-0 gap-2">
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.430 <Button type="button" onClick={onDone} variant="outline">
Chat and workspace agents: channels, DMs and named agents you talk to431 Cancel
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.432 </Button>
433 <Button
Chat and workspace agents: channels, DMs and named agents you talk to434 type="button"
435 disabled={busy || chosen.length === 0}
436 onClick={() => void go()}
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.437 variant="accent"
Chat and workspace agents: channels, DMs and named agents you talk to438 >
439 {busy ? "Opening…" : "Open conversation"}
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.440 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to441 </div>
442 </div>
443 </div>
444 );
445}

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