Skip to content
1,951 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 {
Chat controls, public profiles, shadcn selects, and no Docs tab in a project2 Archive,
3 ArchiveRestore,
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)4 ChevronLeft,
5 Copy,
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread6 Ellipsis,
7 FileText,
8 Link2,
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)9 Pencil,
10 Trash2,
Chat and workspace agents: channels, DMs and named agents you talk to11 BellOff,
12 Hash,
13 Lock,
14 MessageSquareText,
15 PanelRight,
16 Star,
17 UserPlus,
18 Users,
19 X,
20} from "lucide-react";
21import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
Chat controls, public profiles, shadcn selects, and no Docs tab in a project22import { Link, useNavigate, useRouteLoaderData, useSearchParams } from "react-router";
Chat and workspace agents: channels, DMs and named agents you talk to23
Chat controls, public profiles, shadcn selects, and no Docs tab in a project24import {
25 type Channel,
26 type ChannelChange,
27 type ChannelMember,
28 type ChatLiveEvent,
29 type ChatMessage,
30 type MemberProfile,
31 type Result,
32 hasCodeAccess,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar33 shownUsername,
Chat controls, public profiles, shadcn selects, and no Docs tab in a project34} from "@g1t/contracts";
Chat and workspace agents: channels, DMs and named agents you talk to35
36import { useChatData, useChatSend, useChatSidebar } from "./actions";
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread37import { CardBox, CardToasts, showToast } from "./card";
Chat and workspace agents: channels, DMs and named agents you talk to38import { Composer } from "./composer";
39import { type LiveState, useChatLive } from "./live";
40import { AgentPill, MemberAvatar } from "./marks";
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.41import { AgentAvatar } from "../agent-avatar";
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers42import { PersonStatusEmoji, PersonStatusLine, PresenceSummary, WithPresence } from "../presence";
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)43import { CardContext, MemberCard, type PersonCard, personCard } from "./profile-card";
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.44
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)45import { localTime } from "../../lib/time-zone";
46import { BottomSheet, SheetRow, useBack, useSwipeBack } from "../mobile";
Chat and workspace agents: channels, DMs and named agents you talk to47import { MessageText, type TextContext } from "./text";
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread48import { WriteUpDialog } from "./write-up";
Artifacts is a mode in the rail, the shell and the phone's tab bar in place of Docs: its home lists everything you can open by day or as cards under All, Yours and Shared with you, with search, filters and tiles to start a doc (slides, designs and dashboards say they are coming soon), and its sidebar holds favorites, spaces, Private, Shared, projects' docs and the trash.49import { FolioUnfurls } from "../folios/unfurl";
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread50import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../ui/dropdown-menu";
51import { threadLink, writeUpAgents } from "../../lib/write-up";
Chat controls, public profiles, shadcn selects, and no Docs tab in a project52import {
53 AlertDialog,
54 AlertDialogAction,
55 AlertDialogCancel,
56 AlertDialogContent,
57 AlertDialogDescription,
58 AlertDialogFooter,
59 AlertDialogHeader,
60 AlertDialogTitle,
61} from "../ui/alert-dialog";
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.62import { Avatar } from "../ui/avatar";
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar63import { Badge } from "../ui/badge";
Chat and workspace agents: channels, DMs and named agents you talk to64import { Hint } from "../ui/hint";
The top of every page is one surface: the breadcrumb, search, spend, Ask g1t, bell and + share one height, corners and spacing, Ask g1t is a plain tinted button with the g1t mark drawn on whole pixels, a page's own header sits directly under the bar at the same left edge, and every section, conversations and open artifacts included, draws the same rounded panel. Tables written in chat render as tables, and the site's file, checkbox, radio, number and date controls are the design system's. Button, Separator, Breadcrumb, Table and FileDrop join the shared components; the chat guide says how tables work.65import { Button } from "../ui/button";
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.66import { Card } from "../ui/card";
Chat and workspace agents: channels, DMs and named agents you talk to67import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
In a group message with several agents, a message that names no agent goes to the one you are talking with: the agents that answered you last, else the ones you last mentioned, else, before anyone has been addressed, all of them; a message that mentions agents still goes only to those, and a conversation with one agent is as it was. Each agent is also told, when several are here, who you are talking with, so one that is woken alongside answers only what is clearly for it and does not redo a colleague's work. And an agent shows as typing for as long as it works, not for eight seconds: its desk says so every five seconds until the answer is posted, and after ten seconds the line reads that it is still working on it. The chat and agents guides say so.68import {
69 type Mentionable,
70 type ShownMessage,
71 type Typer,
72 WORKING_AFTER_MS,
73 agentsAmong,
74 channelPath,
75 dmPlaceholder,
76 isMixed,
77 mentionNames,
78 mergeMessages,
79 mixedNotice,
80 shownHandle,
81 shownName,
82 timeline,
83 typingLabel,
84} from "../../lib/chat";
Chat and workspace agents: channels, DMs and named agents you talk to85import { codeAccessPath } from "../../lib/workspace-nav";
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)86import { conversationCache } from "./conversation-cache";
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)87import { atBottom, useKeepAtBottom } from "./stick";
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)88// Reactions and the workspace's own emoji (components/emoji).
89import { EmojiProvider, useCustomEmoji } from "../emoji/context";
90import { AddReaction, QuickReactions, ReactionBar } from "../emoji/reaction-bar";
91import { useAddresses } from "../../lib/addresses";
92import { applyReactionEvent, keepMine, toggleReaction } from "../../lib/emoji";
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.93import { dndNow } from "../../lib/notify-client";
94import { chatMessageArrived, messageSent, viewingConversation } from "../../lib/sound-events";
Chat and workspace agents: channels, DMs and named agents you talk to95import type { ChannelData } from "../../routes/workspace/chat/channel";
96
97type Loaded = Extract<ChannelData, { unavailable: false }>;
98
99/** True once the page has hydrated: times and days are the reader's own from then on. */
100function useHydrated(): boolean {
101 const [hydrated, setHydrated] = useState(false);
102 useEffect(() => setHydrated(true), []);
103 return hydrated;
104}
105
106function clock(at: string, zone: string | undefined): string {
107 return new Date(at).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", timeZone: zone });
108}
109
The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were.110/** Whether the viewer uses Code in this workspace. */
Chat and workspace agents: channels, DMs and named agents you talk to111export function useCodeAccess(slug: string): boolean {
112 const root = useRouteLoaderData("root") as { user?: { workspaces?: { slug: string; code_access?: boolean }[] } | null } | undefined;
113 return hasCodeAccess(root?.user?.workspaces?.find((m) => m.slug === slug) ?? null);
114}
115
116/** The project a channel of the same name is about, for a bare `#123`. */
117function useProjectNamed(name: string | null): string | null {
118 const root = useRouteLoaderData("root") as { shell?: { repos?: { name: string }[] } } | undefined;
119 if (!name) return null;
120 return root?.shell?.repos?.some((repo) => repo.name === name) ? name : null;
121}
122
123function newClientId(): string {
124 return `tmp-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
125}
126
127/** A conversation: its header, its messages, the box to write in, and its thread and details beside it. */
128export function ChannelView({ data }: { data: Loaded }) {
129 const chatData = useChatData();
130 const slug = chatData?.slug ?? "";
131 const me = chatData?.me ?? null;
132 const send = useChatSend(slug);
133 const { sidebar, refresh } = useChatSidebar();
134 const hydrated = useHydrated();
135 const zone = hydrated ? undefined : "UTC";
136 const code = useCodeAccess(slug);
137 const project = useProjectNamed(data.channel.kind === "channel" ? data.channel.name : null);
138 const [params, setParams] = useSearchParams();
139 const threadId = params.get("thread");
140
141 const [messages, setMessages] = useState<ShownMessage[]>(() => mergeMessages([], data.messages));
142 const [older, setOlder] = useState<string | null>(data.older);
143 const [loadingOlder, setLoadingOlder] = useState(false);
144 const [thread, setThread] = useState<ShownMessage[] | null>(null);
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread145 // The message the open thread is under, as its page says, for when it is
146 // further back than the conversation has loaded (a long-running session's card).
147 const [threadRoot, setThreadRoot] = useState<ShownMessage | null>(null);
In a group message with several agents, a message that names no agent goes to the one you are talking with: the agents that answered you last, else the ones you last mentioned, else, before anyone has been addressed, all of them; a message that mentions agents still goes only to those, and a conversation with one agent is as it was. Each agent is also told, when several are here, who you are talking with, so one that is woken alongside answers only what is clearly for it and does not redo a colleague's work. And an agent shows as typing for as long as it works, not for eight seconds: its desk says so every five seconds until the answer is posted, and after ten seconds the line reads that it is still working on it. The chat and agents guides say so.148 // Who is typing, until when, and since when (so an agent still at it after a while is said to be working).
149 const [typing, setTyping] = useState<Map<string, { member: MemberProfile; until: number; since: number }>>(new Map());
Chat and workspace agents: channels, DMs and named agents you talk to150 const [joined, setJoined] = useState(data.joined);
151 const [members, setMembers] = useState<ChannelMember[]>(data.members);
Chat controls, public profiles, shadcn selects, and no Docs tab in a project152 // The channel as it is now: renamed, its topic changed, archived, live.
153 const [channel, setChannel] = useState<Channel>(data.channel);
154 const [canManage, setCanManage] = useState(data.can_manage === true);
155 const view = useMemo<Loaded>(() => ({ ...data, channel, title: channel.kind === "channel" ? `#${channel.name}` : data.title }), [data, channel]);
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)156 // The server's answer behind a cached draw: merged in by id.
157 useEffect(() => {
158 const onFresh = (event: Event) => {
159 const { key, value } = (event as CustomEvent<{ key: string; value: ChannelData }>).detail;
160 if (key !== window.location.pathname || value.unavailable || value.channel.id !== data.channel.id) return;
161 setMessages((now) => mergeMessages(now, value.messages));
162 setMembers(value.members);
163 setJoined(value.joined);
Chat controls, public profiles, shadcn selects, and no Docs tab in a project164 setChannel(value.channel);
165 setCanManage(value.can_manage === true);
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)166 };
167 window.addEventListener("g1t:chat-fresh", onFresh);
168 return () => window.removeEventListener("g1t:chat-fresh", onFresh);
169 }, [data.channel.id]);
170 // The cache follows what is on screen, so coming back shows the latest.
171 useEffect(() => {
172 const path = window.location.pathname;
173 const timer = setTimeout(
Chat controls, public profiles, shadcn selects, and no Docs tab in a project174 () => conversationCache.set(path, { ...view, messages: messages.filter((m) => !m.pending).slice(-80), members, joined, can_manage: canManage }),
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)175 400,
176 );
177 return () => clearTimeout(timer);
Chat controls, public profiles, shadcn selects, and no Docs tab in a project178 }, [view, messages, members, joined, canManage]);
179 // Renamed while open, here or by someone else: the address follows the name.
180 const navigate = useNavigate();
181 const nameShown = useRef(data.channel.name);
182 useEffect(() => {
183 if (channel.kind !== "channel" || channel.name === nameShown.current || !slug) return;
184 nameShown.current = channel.name;
185 navigate(`${channelPath(slug, channel)}${window.location.search}`, { replace: true, preventScrollReset: true });
186 }, [channel, slug, navigate]);
Chat and workspace agents: channels, DMs and named agents you talk to187 const [info, setInfo] = useState(false);
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)188 // A phone: back to the list, by the chevron or a swipe from the edge;
189 // and a long-pressed message's actions.
190 const back = useBack(`/${chatData?.slug ?? ""}/-/chat`);
191 const swipe = useSwipeBack(back);
192 const [actions, setActions] = useState<ShownMessage | null>(null);
193 // Someone's profile, opened from their card, beside the conversation.
194 const [profile, setProfile] = useState<string | null>(null);
Merge main into Artifacts Phase 2195 // A message being edited: in its place on a computer (`inline`), in a sheet on a phone.
196 const [editing, setEditing] = useState<{ id: string; body: string; inline?: boolean } | null>(null);
Chat and workspace agents: channels, DMs and named agents you talk to197 const scroller = useRef<HTMLDivElement>(null);
198 const stick = useRef(true);
199
200 const entry = sidebar?.entries.find((e) => e.channel.id === data.channel.id);
201 const starred = entry?.starred ?? members.find((m) => m.member.kind === "user" && m.member.id === me?.id)?.starred ?? false;
202 const muted = entry?.muted ?? false;
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.203
204 // Sounds know which conversation is on screen: it is silent while looked at (lib/sound-events.ts).
205 useEffect(() => {
206 viewingConversation(data.channel.id);
207 return () => viewingConversation(null);
208 }, [data.channel.id]);
Chat and workspace agents: channels, DMs and named agents you talk to209 const others = members.filter((m) => !(m.member.kind === "user" && m.member.id === me?.id)).map((m) => m.member);
210 const isDm = data.channel.kind === "dm";
211
212 // The details panel: open by default on a wide screen, as last left.
213 useEffect(() => {
214 try {
215 const saved = localStorage.getItem("chat-info");
216 setInfo(saved ? saved === "1" : window.matchMedia("(min-width: 1280px)").matches && !isDm);
217 } catch {
218 setInfo(false);
219 }
220 }, [isDm]);
221 const toggleInfo = () =>
222 setInfo((now) => {
223 try {
224 localStorage.setItem("chat-info", now ? "0" : "1");
225 } catch {
226 // Not kept: fine.
227 }
228 return !now;
229 });
230
231 const people = useMemo<Mentionable[]>(
232 () => [
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.233 // A personal agent is never mentioned: it answers only in its own DM.
234 ...(chatData?.agents ?? [])
235 .filter((agent) => agent.scope !== "personal")
236 .map((agent) => ({ kind: "agent" as const, name: agent.handle, display_name: agent.display_name, avatar: agent.avatar, role: agent.role })),
Chat and workspace agents: channels, DMs and named agents you talk to237 ...(chatData?.people ?? []).filter((p) => p.name !== me?.username),
238 ],
239 [chatData, me?.username],
240 );
241 const context = useMemo<TextContext>(
242 () => ({
243 slug,
244 me: me?.username ?? null,
245 agents: new Set((chatData?.agents ?? []).map((a) => a.handle.toLowerCase())),
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar246 // `@ana` reads as the name people know: everyone in the workspace, the viewer too.
247 names: mentionNames([...people, ...(me ? [{ kind: "user" as const, name: me.username, display_username: me.display_username, display_name: me.display_name, avatar: me.avatar }] : [])]),
Chat and workspace agents: channels, DMs and named agents you talk to248 channels: new Set((sidebar?.entries ?? []).filter((e) => e.channel.kind === "channel").map((e) => e.channel.name ?? "")),
249 project,
250 codeLink: code ? undefined : (href: string) => (/^\/[^/]+\/(?!-\/)[^/]+/.test(href) ? codeAccessPath(slug, href) : href),
251 }),
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar252 [slug, me, people, chatData?.agents, sidebar?.entries, project, code],
Chat and workspace agents: channels, DMs and named agents you talk to253 );
254
255 // Keeps the newest message in view while the reader is at the bottom.
256 const toBottom = useCallback((smooth = false) => {
257 const element = scroller.current;
258 if (element) element.scrollTo({ top: element.scrollHeight, behavior: smooth ? "smooth" : "auto" });
259 }, []);
260 useEffect(() => toBottom(), [toBottom]);
261 useEffect(() => {
262 if (stick.current) toBottom(true);
263 }, [messages.length, typing.size, toBottom]);
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)264 // And while the list or what is in it changes size: the keyboard opening on a phone, a card laying out.
265 useKeepAtBottom(scroller, stick);
Chat and workspace agents: channels, DMs and named agents you talk to266
267 // Read up to the newest message whenever it is in view.
268 const lastRead = useRef<string | null>(null);
269 const markRead = useCallback(() => {
270 if (!joined || document.visibilityState !== "visible") return;
271 const newest = [...messages].reverse().find((m) => !m.pending);
272 if (!newest || newest.id === lastRead.current) return;
273 lastRead.current = newest.id;
274 void send({ intent: "read", channel_id: data.channel.id, id: newest.id });
275 window.dispatchEvent(new CustomEvent("g1t:chat-read", { detail: data.channel.id }));
276 }, [messages, joined, send, data.channel.id]);
277 useEffect(() => {
278 const timer = setTimeout(markRead, 400);
279 document.addEventListener("visibilitychange", markRead);
280 window.addEventListener("focus", markRead);
281 return () => {
282 clearTimeout(timer);
283 document.removeEventListener("visibilitychange", markRead);
284 window.removeEventListener("focus", markRead);
285 };
286 }, [markRead]);
287
288 // The thread open beside the conversation, from `?thread=`.
289 const loadThread = useCallback(async () => {
290 if (!threadId) return setThread(null);
291 try {
292 const response = await fetch(`/${slug}/-/chat/api?channel=${encodeURIComponent(data.channel.id)}&thread=${encodeURIComponent(threadId)}`);
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread293 const result = (await response.json()) as Result<{ messages: ChatMessage[]; root?: ChatMessage | null }>;
Chat and workspace agents: channels, DMs and named agents you talk to294 // The page reaches back to the message the thread is under: shown above it, not among the replies.
295 setThread(result.ok ? mergeMessages([], result.value.messages.filter((m) => m.id !== threadId && !m.deleted_at)) : []);
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread296 const root = result.ok ? (result.value.root ?? result.value.messages.find((m) => m.id === threadId) ?? null) : null;
297 setThreadRoot(root);
Chat and workspace agents: channels, DMs and named agents you talk to298 } catch {
299 setThread([]);
300 }
301 }, [threadId, slug, data.channel.id]);
302 useEffect(() => {
303 setThread(null);
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread304 setThreadRoot(null);
Chat and workspace agents: channels, DMs and named agents you talk to305 void loadThread();
306 }, [loadThread]);
307
308 // Reply counts: counted here only until the service's own count for that
309 // message arrives (a `message.updated` of it), which can come before or
310 // after the reply itself; never twice for one reply.
311 const serverCounted = useRef(new Set<string>());
312 const countedReplies = useRef(new Set<string>());
313 const countReply = useCallback((root: string, reply: string, at: string) => {
314 if (serverCounted.current.has(root) || countedReplies.current.has(reply)) return;
315 countedReplies.current.add(reply);
316 setMessages((now) => now.map((m) => (m.id === root ? { ...m, reply_count: m.reply_count + 1, last_reply_at: at } : m)));
317 }, []);
318 const latest = useRef(messages);
319 latest.current = messages;
320
321 // Everything that happens while the page is open.
322 const onEvent = useCallback(
323 (event: ChatLiveEvent) => {
324 if (event.type === "message.created" || event.type === "message.updated") {
325 const message = event.message;
326 if (message.channel_id !== data.channel.id) return;
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.327 // A sound, when the rules say so: not for your own, not while looking at it in front.
328 if (event.type === "message.created") chatMessageArrived(message, data.channel.kind, { dnd: dndNow(), muted });
Chat and workspace agents: channels, DMs and named agents you talk to329 if (message.thread_root) {
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)330 if (message.thread_root === threadId) setThread((now) => (now ? mergeMessages(now, [keepMine(now, message)]) : now));
Chat and workspace agents: channels, DMs and named agents you talk to331 if (event.type === "message.created") countReply(message.thread_root, message.id, message.created_at);
332 } else {
333 if (event.type === "message.updated") serverCounted.current.add(message.id);
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)334 setMessages((now) => mergeMessages(now, [keepMine(now, message)]));
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread335 // The open thread's root, a session's card say, changes in place at its top too.
336 if (message.id === threadId) setThreadRoot((now) => (now ? keepMine([now], message) : now));
Chat and workspace agents: channels, DMs and named agents you talk to337 }
338 // Whoever just spoke is no longer typing.
339 setTyping((now) => {
340 const key = `${message.author.kind}:${message.author.id}`;
341 if (!now.has(key)) return now;
342 const next = new Map(now);
343 next.delete(key);
344 return next;
345 });
346 } else if (event.type === "message.deleted") {
347 if (event.channel_id !== data.channel.id) return;
348 const gone = (m: ShownMessage) => (m.id === event.id ? { ...m, deleted_at: new Date().toISOString() } : m);
349 setMessages((now) => now.map(gone));
350 setThread((now) => now?.map(gone) ?? now);
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread351 setThreadRoot((now) => (now ? gone(now) : now));
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 } else if (event.type === "reaction.added" || event.type === "reaction.removed") {
353 if (event.channel_id !== data.channel.id) return;
354 setMessages((now) => applyReactionEvent(now, event, me?.id ?? null));
355 setThread((now) => (now ? applyReactionEvent(now, event, me?.id ?? null) : now));
Chat and workspace agents: channels, DMs and named agents you talk to356 } else if (event.type === "typing") {
357 if (event.channel_id !== data.channel.id) return;
358 if (event.member.kind === "user" && event.member.id === me?.id) return;
In a group message with several agents, a message that names no agent goes to the one you are talking with: the agents that answered you last, else the ones you last mentioned, else, before anyone has been addressed, all of them; a message that mentions agents still goes only to those, and a conversation with one agent is as it was. Each agent is also told, when several are here, who you are talking with, so one that is woken alongside answers only what is clearly for it and does not redo a colleague's work. And an agent shows as typing for as long as it works, not for eight seconds: its desk says so every five seconds until the answer is posted, and after ten seconds the line reads that it is still working on it. The chat and agents guides say so.359 setTyping((now) => {
360 const key = `${event.member.kind}:${event.member.id}`;
361 // An agent working on a long reply says it is typing again every few seconds: one stretch, from its first.
362 return new Map(now).set(key, { member: event.member, until: new Date(event.until).getTime(), since: now.get(key)?.since ?? Date.now() });
363 });
Chat controls, public profiles, shadcn selects, and no Docs tab in a project364 } else if (event.type === "channel.updated") {
365 // Renamed, a new topic, archived or back: everyone looking sees it at once.
366 if (event.channel.id !== data.channel.id) return;
367 setChannel(event.channel);
368 refresh();
Chat and workspace agents: channels, DMs and named agents you talk to369 }
370 },
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.371 [data.channel.id, data.channel.kind, muted, threadId, me?.id, countReply, refresh],
Chat and workspace agents: channels, DMs and named agents you talk to372 );
373 // Back after a drop: what was missed.
374 const onReconnect = useCallback(async () => {
375 const base = `/${slug}/-/chat/api?channel=${encodeURIComponent(data.channel.id)}`;
376 try {
377 // Everything after the newest message held, page by page, oldest first;
378 // what was deleted meanwhile comes back empty and is dropped.
379 let after = [...latest.current].reverse().find((m) => !m.pending)?.id ?? null;
380 for (let pages = 0; after && pages < 20; pages++) {
381 const response = await fetch(`${base}&after=${encodeURIComponent(after)}`);
382 const result = (await response.json()) as Result<{ messages: ChatMessage[]; newer?: string | null }>;
383 if (!result.ok) break;
384 const gone = new Set(result.value.messages.filter((m) => m.deleted_at).map((m) => m.id));
385 const kept = result.value.messages.filter((m) => !m.deleted_at && !m.thread_root);
386 setMessages((now) => mergeMessages(now.filter((m) => !gone.has(m.id)), kept));
387 after = result.value.newer ?? null;
388 }
389 // Edits and deletions to what was already on screen: the latest page again.
390 const response = await fetch(base);
391 const result = (await response.json()) as Result<{ messages: ChatMessage[] }>;
392 if (result.ok) {
393 const gone = new Set(result.value.messages.filter((m) => m.deleted_at).map((m) => m.id));
394 setMessages((now) => mergeMessages(now.filter((m) => !gone.has(m.id)), result.value.messages.filter((m) => !m.deleted_at)));
395 }
396 } catch {
397 // The next event, or the next return, catches up.
398 }
399 void loadThread();
400 refresh();
401 }, [slug, data.channel.id, loadThread, refresh]);
402 const live = useChatLive(slug, data.channel.id, onEvent, onReconnect);
403
404 // Typing shows until its time runs out.
405 useEffect(() => {
406 if (typing.size === 0) return;
407 const timer = setInterval(() => {
408 const now = Date.now();
409 setTyping((current) => {
410 const next = new Map([...current].filter(([, value]) => value.until > now));
411 return next.size === current.size ? current : next;
412 });
413 }, 1000);
414 return () => clearInterval(timer);
415 }, [typing.size]);
416
417 // Says "is typing" to the others, at most every few seconds.
418 const lastTyping = useRef(0);
419 const onTyping = useCallback(() => {
420 const now = Date.now();
421 if (now - lastTyping.current < 3000) return;
422 lastTyping.current = now;
423 live.send({ type: "typing", channel_id: data.channel.id });
424 }, [live, data.channel.id]);
425
426 const post = useCallback(
427 async (body: string, root: string | null) => {
428 if (!me) return;
429 const clientId = newClientId();
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar430 const author: MemberProfile = { kind: "user", id: me.id, name: me.username, display_username: me.display_username, display_name: me.display_name, avatar: me.avatar, role: null };
Chat and workspace agents: channels, DMs and named agents you talk to431 const pending: ShownMessage = {
432 id: clientId,
433 client_id: clientId,
434 pending: true,
435 channel_id: data.channel.id,
436 author,
437 kind: "text",
438 body,
439 card: null,
440 thread_root: root,
441 reply_count: 0,
442 last_reply_at: null,
443 created_at: new Date().toISOString(),
444 edited_at: null,
445 deleted_at: null,
446 };
447 const put = (list: ShownMessage[]) => mergeMessages(list, [pending]);
448 if (root) setThread((now) => put(now ?? []));
449 else {
450 stick.current = true;
451 setMessages(put);
452 }
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.453 messageSent({ dnd: dndNow() });
Chat and workspace agents: channels, DMs and named agents you talk to454 const saved = await send<ChatMessage>({ intent: "post", channel_id: data.channel.id, body, thread_root: root });
455 const settle = (list: ShownMessage[]) =>
456 saved.ok
457 ? mergeMessages(list, [{ ...saved.value, client_id: clientId }])
458 : list.map((m) => (m.client_id === clientId ? { ...m, failed: true, pending: true } : m));
459 if (root) {
460 setThread((now) => (now ? settle(now) : now));
461 if (saved.ok) countReply(root, saved.value.id, saved.value.created_at);
462 } else setMessages(settle);
463 },
464 [me, send, data.channel.id, countReply],
465 );
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)466 // ── Reactions (components/emoji): shown at once, then as the service counts them.
467 const customs = useCustomEmoji(slug);
468 const { usercontent } = useAddresses();
469 const meProfile = useMemo<MemberProfile | null>(
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar470 () => (me ? { kind: "user", id: me.id, name: me.username, display_username: me.display_username, display_name: me.display_name, avatar: me.avatar, role: null } : null),
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)471 [me],
472 );
473 const react = useCallback(
474 async (messageId: string, emoji: string, on: boolean) => {
475 if (!meProfile) return;
476 const change = (to: boolean) => (list: ShownMessage[]) =>
477 list.map((m) => (m.id === messageId ? { ...m, reactions: toggleReaction(m.reactions ?? [], emoji, to, meProfile) } : m));
478 setMessages(change(on));
479 setThread((now) => (now ? change(on)(now) : now));
480 try {
481 const response = await fetch(`/${slug}/-/chat/api`, {
482 method: "POST",
483 headers: { "content-type": "application/json" },
484 body: JSON.stringify({ intent: on ? "react" : "unreact", channel_id: data.channel.id, message_id: messageId, emoji }),
485 });
486 const result = (await response.json()) as Result<ChatMessage["reactions"]>;
487 if (!result.ok) throw new Error(result.error.message);
488 const settle = (list: ShownMessage[]) => list.map((m) => (m.id === messageId ? { ...m, reactions: result.value } : m));
489 setMessages(settle);
490 setThread((now) => (now ? settle(now) : now));
491 } catch {
492 // Not saved: back as it was.
493 setMessages(change(!on));
494 setThread((now) => (now ? change(!on)(now) : now));
495 }
496 },
497 [meProfile, slug, data.channel.id],
498 );
499 const emojiContext = useMemo(
500 () => ({ customs, usercontent, me: meProfile, react: (id: string, emoji: string, on: boolean) => void react(id, emoji, on), manageHref: `/${slug}/-/emoji` }),
501 [customs, usercontent, meProfile, react, slug],
502 );
503
Chat and workspace agents: channels, DMs and named agents you talk to504 const retry = (message: ShownMessage) => {
505 const drop = (list: ShownMessage[]) => list.filter((m) => m.client_id !== message.client_id);
506 if (message.thread_root) setThread((now) => (now ? drop(now) : now));
507 else setMessages(drop);
508 void post(message.body, message.thread_root);
509 };
510
511 const loadOlder = async () => {
512 if (!older || loadingOlder) return;
513 setLoadingOlder(true);
514 const element = scroller.current;
515 const fromBottom = element ? element.scrollHeight - element.scrollTop : 0;
516 try {
517 const response = await fetch(`/${slug}/-/chat/api?channel=${encodeURIComponent(data.channel.id)}&before=${encodeURIComponent(older)}`);
518 const result = (await response.json()) as Result<{ messages: ChatMessage[]; older: string | null }>;
519 if (result.ok) {
520 stick.current = false;
521 setMessages((now) => mergeMessages(now, result.value.messages));
522 setOlder(result.value.older);
523 requestAnimationFrame(() => {
524 if (element) element.scrollTop = element.scrollHeight - fromBottom;
525 });
526 }
527 } finally {
528 setLoadingOlder(false);
529 }
530 };
531
532 const openThread = (id: string | null) => {
533 const next = new URLSearchParams(params);
534 if (id) next.set("thread", id);
535 else next.delete("thread");
536 setParams(next, { preventScrollReset: true, replace: !!threadId && !!id });
537 };
538
Artifacts is a mode in the rail, the shell and the phone's tab bar in place of Docs: its home lists everything you can open by day or as cards under All, Yours and Shared with you, with search, filters and tiles to start a doc (slides, designs and dashboards say they are coming soon), and its sidebar holds favorites, spaces, Private, Shared, projects' docs and the trash.539 // A thread's link, to copy and to cite; and "Write this up as an artifact",
540 // which asks an agent, in the thread and as the person, for a doc about it.
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread541 const [writeUp, setWriteUp] = useState<string | null>(null);
542 const linkToThread = (root: string) => threadLink(window.location.origin, channelPath(slug, channel), root);
543 const copyThreadLink = (root: string) => {
544 const done = navigator.clipboard?.writeText(linkToThread(root));
545 if (!done) return showToast(false, "Couldn't copy the link here.");
546 done.then(
547 () => showToast(true, "Link to the thread copied."),
548 () => showToast(false, "Couldn't copy the link here."),
549 );
550 };
551 const writers = useMemo(() => writeUpAgents(members.map((m) => m.member)), [members]);
552 const askForWriteUp = async (body: string): Promise<string | null> => {
553 const root = writeUp;
554 if (!root) return null;
555 // The thread open already: posted as any reply is, shown at once.
556 if (root === threadId) {
557 void post(body, root);
558 return null;
559 }
560 const saved = await send<ChatMessage>({ intent: "post", channel_id: data.channel.id, body, thread_root: root });
561 if (!saved.ok) return saved.error.message;
562 openThread(root);
563 return null;
564 };
565
Chat controls, public profiles, shadcn selects, and no Docs tab in a project566 const updateChannel = useCallback(
567 async (change: ChannelChange): Promise<Result<unknown>> => {
568 const done = await send<{ channel: Channel }>({ intent: "update_channel", channel_id: data.channel.id, ...change });
569 if (done.ok) setChannel(done.value.channel);
570 return done;
571 },
572 [send, data.channel.id],
573 );
574
Chat and workspace agents: channels, DMs and named agents you talk to575 const toggleStar = async () => {
576 await send({ intent: "preferences", channel_id: data.channel.id, starred: !starred });
577 refresh();
578 };
579
580 const join = async () => {
581 const done = await send({ intent: "join", channel_id: data.channel.id });
582 if (done.ok) {
583 setJoined(true);
584 if (me) {
585 setMembers((now) => [
586 ...now,
587 {
588 channel_id: data.channel.id,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar589 member: { kind: "user", id: me.id, name: me.username, display_username: me.display_username, display_name: me.display_name, avatar: me.avatar, role: null },
Chat and workspace agents: channels, DMs and named agents you talk to590 role: "member",
591 starred: false,
592 muted: false,
593 last_read_id: null,
594 joined_at: new Date().toISOString(),
595 },
596 ]);
597 }
598 }
599 };
600
601 const rows = useMemo(() => timeline(messages, new Date(), zone), [messages, zone]);
Merge main into Artifacts Phase 2602 const mine = (message: ShownMessage) => message.author.kind === "user" && message.author.id === me?.id && !message.pending && !message.deleted_at;
603 // Editing uses the composer, started from the message's Markdown.
604 const editor = (target: { id: string; body: string }) => (
605 <Composer
606 initial={target.body}
607 placeholder="Edit message"
608 people={people}
609 autoFocus
610 compact
611 onCancel={() => setEditing(null)}
612 onSend={async (body) => {
613 setEditing(null);
614 if (body === target.body) return;
615 // Shown edited at once; put back if the service refuses.
616 const before = messages.find((m) => m.id === target.id);
617 setMessages((now) => now.map((m) => (m.id === target.id ? { ...m, body, edited_at: new Date().toISOString() } : m)));
618 const saved = await send<ChatMessage>({ intent: "edit", channel_id: data.channel.id, id: target.id, body });
619 if (saved.ok) setMessages((now) => mergeMessages(now, [saved.value]));
620 else if (before) setMessages((now) => now.map((m) => (m.id === before.id ? before : m)));
621 }}
622 />
623 );
In a group message with several agents, a message that names no agent goes to the one you are talking with: the agents that answered you last, else the ones you last mentioned, else, before anyone has been addressed, all of them; a message that mentions agents still goes only to those, and a conversation with one agent is as it was. Each agent is also told, when several are here, who you are talking with, so one that is woken alongside answers only what is clearly for it and does not redo a colleague's work. And an agent shows as typing for as long as it works, not for eight seconds: its desk says so every five seconds until the answer is posted, and after ten seconds the line reads that it is still working on it. The chat and agents guides say so.624 const typers = [...typing.values()];
Chat controls, public profiles, shadcn selects, and no Docs tab in a project625 const name = channel.name ?? "";
626 const archived = channel.kind === "channel" && !!channel.archived_at;
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.627 // A mixed direct message names each member and says which are agents.
Chat and workspace agents: channels, DMs and named agents you talk to628 const placeholder = isDm
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.629 ? isMixed(others)
630 ? dmPlaceholder(others)
631 : `Message ${others.map(shownName).join(", ") || "yourself"}`
Chat and workspace agents: channels, DMs and named agents you talk to632 : `Message #${name}. @ a teammate or an agent`;
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread633 const rootMessage = threadId ? (messages.find((m) => m.id === threadId) ?? (threadRoot?.id === threadId ? threadRoot : null)) : null;
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)634 const cardContext = useMemo(
635 () => ({
636 slug,
637 agents: chatData?.agents ?? [],
638 onViewProfile: (username: string) => {
639 if (threadId) openThread(null);
640 setProfile(username);
641 },
642 }),
643 // eslint-disable-next-line react-hooks/exhaustive-deps
644 [slug, chatData?.agents, threadId],
645 );
Chat and workspace agents: channels, DMs and named agents you talk to646
647 return (
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)648 // On a phone the conversation is the whole screen, as tall as what the
649 // keyboard leaves (components/mobile.tsx), so the composer sits on it.
650 <EmojiProvider value={emojiContext}>
651 <CardContext.Provider value={cardContext}>
652 <div
653 {...swipe}
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.654 className="flex h-(--page-h) min-h-0 max-md:fixed max-md:inset-x-0 max-md:top-(--vv-top,0px) max-md:z-30 max-md:h-(--vv-height,100dvh) max-md:bg-bg"
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)655 >
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar656 {/* What pressing a card's action did (components/chat/card.tsx). */}
657 <CardToasts />
Chat and workspace agents: channels, DMs and named agents you talk to658 <section aria-label={data.title} className="flex min-w-0 grow flex-col">
659 <ChannelHeader
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)660 onBack={back}
Chat controls, public profiles, shadcn selects, and no Docs tab in a project661 data={view}
Chat and workspace agents: channels, DMs and named agents you talk to662 others={others}
663 starred={starred}
664 muted={muted}
665 memberCount={members.length}
666 live={live.state}
667 info={info}
668 onStar={() => void toggleStar()}
669 onInfo={toggleInfo}
670 />
671 <div
672 ref={scroller}
673 onScroll={(event) => {
674 const element = event.currentTarget;
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)675 stick.current = atBottom(element);
Chat and workspace agents: channels, DMs and named agents you talk to676 }}
677 className="min-h-0 grow overflow-y-auto overscroll-contain [scrollbar-gutter:stable] [scrollbar-width:thin]"
678 >
679 <div className="mx-auto flex min-h-full max-w-[56rem] flex-col justify-end px-2 pt-6 pb-3 sm:px-4">
680 {older ? (
681 <div className="flex justify-center pb-4">
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.682 <Button
Chat and workspace agents: channels, DMs and named agents you talk to683 type="button"
684 onClick={() => void loadOlder()}
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.685 variant="outline"
686 size="inline"
687 className="rounded-full px-3 py-1 text-xs text-muted font-normal"
Chat and workspace agents: channels, DMs and named agents you talk to688 >
689 {loadingOlder ? "Loading…" : "Load earlier messages"}
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.690 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to691 </div>
692 ) : (
Chat controls, public profiles, shadcn selects, and no Docs tab in a project693 <ConversationStart data={view} others={others} />
Chat and workspace agents: channels, DMs and named agents you talk to694 )}
695 {rows.map((row) =>
696 row.kind === "day" ? (
697 <DayRule key={row.key} label={row.label} />
698 ) : (
699 <MessageRow
700 key={row.key}
701 message={row.message}
702 head={row.head}
703 zone={zone}
704 context={context}
705 code={code}
706 slug={slug}
707 onThread={() => openThread(row.message.id)}
708 onRetry={() => retry(row.message)}
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)709 onLongPress={() => setActions(row.message)}
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread710 onCopyLink={() => copyThreadLink(row.message.id)}
Merge main into Artifacts Phase 2711 onEdit={mine(row.message) ? () => setEditing({ id: row.message.id, body: row.message.body, inline: true }) : undefined}
712 editor={editing?.inline && editing.id === row.message.id ? editor(editing) : undefined}
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread713 onWriteUp={joined && !archived ? () => setWriteUp(row.message.id) : undefined}
Chat and workspace agents: channels, DMs and named agents you talk to714 threadOpen={row.message.id === threadId}
715 />
716 ),
717 )}
718 </div>
719 </div>
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)720 <div className="shrink-0 px-2 pb-3 sm:px-4 sm:pb-4 max-md:pb-[max(0.75rem,env(safe-area-inset-bottom))] in-data-[keyboard=open]:pb-2">
Chat and workspace agents: channels, DMs and named agents you talk to721 <div className="mx-auto max-w-[56rem]">
In a group message with several agents, a message that names no agent goes to the one you are talking with: the agents that answered you last, else the ones you last mentioned, else, before anyone has been addressed, all of them; a message that mentions agents still goes only to those, and a conversation with one agent is as it was. Each agent is also told, when several are here, who you are talking with, so one that is woken alongside answers only what is clearly for it and does not redo a colleague's work. And an agent shows as typing for as long as it works, not for eight seconds: its desk says so every five seconds until the answer is posted, and after ten seconds the line reads that it is still working on it. The chat and agents guides say so.722 <TypingLine typers={typers} />
Chat controls, public profiles, shadcn selects, and no Docs tab in a project723 {archived ? (
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.724 <Card className="flex flex-wrap items-center justify-between gap-3 px-4 py-3">
Chat controls, public profiles, shadcn selects, and no Docs tab in a project725 <p className="flex items-center gap-2 text-sm text-muted">
726 <Archive size={15} className="shrink-0 text-faint" />
727 <span>
728 <span className="font-medium text-fg">#{name}</span> is archived. Its history stays here to read; nobody can post in it.
729 </span>
730 </p>
731 {canManage && (
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.732 <Button
Chat controls, public profiles, shadcn selects, and no Docs tab in a project733 type="button"
734 onClick={() => void updateChannel({ archived: false })}
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.735 variant="outline"
736 className="px-3 text-fg/90 hover:bg-raised"
Chat controls, public profiles, shadcn selects, and no Docs tab in a project737 >
738 <ArchiveRestore size={15} />
739 Unarchive
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.740 </Button>
Chat controls, public profiles, shadcn selects, and no Docs tab in a project741 )}
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.742 </Card>
Chat controls, public profiles, shadcn selects, and no Docs tab in a project743 ) : joined ? (
Chat and workspace agents: channels, DMs and named agents you talk to744 <Composer draftKey={data.channel.id} placeholder={placeholder} people={people} onSend={(body) => void post(body, null)} onTyping={onTyping} autoFocus />
745 ) : (
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.746 <Card className="flex flex-wrap items-center justify-between gap-3 px-4 py-3">
Chat and workspace agents: channels, DMs and named agents you talk to747 <p className="text-sm text-muted">
748 You're reading <span className="font-medium text-fg">#{name}</span>. Join to post and get its notifications.
749 </p>
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.750 <Button
Chat and workspace agents: channels, DMs and named agents you talk to751 type="button"
752 onClick={() => void join()}
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.753 variant="accent"
Chat and workspace agents: channels, DMs and named agents you talk to754 >
755 Join 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.756 </Button>
757 </Card>
Chat and workspace agents: channels, DMs and named agents you talk to758 )}
759 </div>
760 </div>
761 </section>
762 {threadId ? (
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread763 <SidePanel
764 title="Thread"
765 subtitle={isDm ? undefined : `#${name}`}
766 onClose={() => openThread(null)}
767 menu={<ThreadMenu onCopyLink={() => copyThreadLink(threadId)} onWriteUp={joined && !archived ? () => setWriteUp(threadId) : undefined} />}
768 >
Chat and workspace agents: channels, DMs and named agents you talk to769 <ThreadPanel
770 root={rootMessage}
771 replies={thread}
772 zone={zone}
773 context={context}
774 code={code}
775 slug={slug}
776 people={people}
Chat controls, public profiles, shadcn selects, and no Docs tab in a project777 joined={joined && !archived}
Chat and workspace agents: channels, DMs and named agents you talk to778 onSend={(body) => void post(body, threadId)}
779 onRetry={retry}
Chat controls, public profiles, shadcn selects, and no Docs tab in a project780 onTyping={onTyping}
Chat and workspace agents: channels, DMs and named agents you talk to781 draftKey={`${data.channel.id}:${threadId}`}
782 />
783 </SidePanel>
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)784 ) : profile ? (
785 <SidePanel title="Profile" onClose={() => setProfile(null)}>
786 <ProfilePanel slug={slug} username={profile} messages={messages} zone={zone} context={context} />
787 </SidePanel>
Chat and workspace agents: channels, DMs and named agents you talk to788 ) : (
789 info && (
790 <SidePanel title={isDm ? "Details" : "About this channel"} onClose={toggleInfo}>
Chat controls, public profiles, shadcn selects, and no Docs tab in a project791 <InfoPanel
792 data={view}
793 members={members}
794 slug={slug}
795 people={people}
796 onAdded={(m) => setMembers((now) => [...now, m])}
797 joined={joined}
798 canManage={canManage}
799 onUpdate={updateChannel}
800 />
Chat and workspace agents: channels, DMs and named agents you talk to801 </SidePanel>
802 )
803 )}
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)804 <MessageActions
805 message={actions}
806 mine={actions != null && actions.author.kind === "user" && actions.author.id === me?.id}
807 onClose={() => setActions(null)}
808 onThread={(id) => openThread(id)}
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread809 onCopyLink={(message) => copyThreadLink(message.thread_root ?? message.id)}
810 onWriteUp={joined && !archived ? (message) => setWriteUp(message.thread_root ?? message.id) : undefined}
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)811 onEdit={(message) => setEditing({ id: message.id, body: message.body })}
812 onDelete={async (message) => {
813 // Gone at once; back if the service refuses.
814 setMessages((now) => now.map((m) => (m.id === message.id ? { ...m, deleted_at: new Date().toISOString() } : m)));
815 const done = await send({ intent: "remove", channel_id: data.channel.id, id: message.id });
816 if (!done.ok) setMessages((now) => now.map((m) => (m.id === message.id ? { ...m, deleted_at: null } : m)));
817 }}
818 />
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread819 <WriteUpDialog
820 open={writeUp != null}
821 onOpenChange={(open) => !open && setWriteUp(null)}
822 slug={slug}
823 agents={writers}
824 link={writeUp && typeof window !== "undefined" ? linkToThread(writeUp) : ""}
Artifacts is a mode in the rail, the shell and the phone's tab bar in place of Docs: its home lists everything you can open by day or as cards under All, Yours and Shared with you, with search, filters and tiles to start a doc (slides, designs and dashboards say they are coming soon), and its sidebar holds favorites, spaces, Private, Shared, projects' docs and the trash.825 shared={channel.kind === "dm" || channel.private}
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread826 onSend={askForWriteUp}
827 />
Merge main into Artifacts Phase 2828 <BottomSheet open={editing != null && !editing.inline} onOpenChange={(open) => !open && setEditing(null)} title="Edit message">
829 {editing && !editing.inline && (
830 <div className="space-y-3 px-1 pt-1">
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)831 <h2 className="px-2 text-base font-semibold">Edit message</h2>
Merge main into Artifacts Phase 2832 {editor(editing)}
833 </div>
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)834 )}
835 </BottomSheet>
Chat and workspace agents: channels, DMs and named agents you talk to836 </div>
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)837 </CardContext.Provider>
838 </EmojiProvider>
Chat and workspace agents: channels, DMs and named agents you talk to839 );
840}
841
842/** The top of a conversation: what it is, and its controls. */
843function ChannelHeader({
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)844 onBack,
Chat and workspace agents: channels, DMs and named agents you talk to845 data,
846 others,
847 starred,
848 muted,
849 memberCount,
850 live,
851 info,
852 onStar,
853 onInfo,
854}: {
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)855 /** A phone: back to the list of conversations. */
856 onBack: () => void;
Chat and workspace agents: channels, DMs and named agents you talk to857 data: Loaded;
858 others: MemberProfile[];
859 starred: boolean;
860 muted: boolean;
861 memberCount: number;
862 live: LiveState;
863 info: boolean;
864 onStar: () => void;
865 onInfo: () => void;
866}) {
867 const channel = data.channel;
868 const agentDm = channel.kind === "dm" && others.length === 1 && others[0]!.kind === "agent" ? others[0]! : null;
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.869 const mixed = channel.kind === "dm" && isMixed(others);
Chat and workspace agents: channels, DMs and named agents you talk to870 return (
The top of every page is one surface: the breadcrumb, search, spend, Ask g1t, bell and + share one height, corners and spacing, Ask g1t is a plain tinted button with the g1t mark drawn on whole pixels, a page's own header sits directly under the bar at the same left edge, and every section, conversations and open artifacts included, draws the same rounded panel. Tables written in chat render as tables, and the site's file, checkbox, radio, number and date controls are the design system's. Button, Separator, Breadcrumb, Table and FileDrop join the shared components; the chat guide says how tables work.871 <header data-page-head className="flex h-14 shrink-0 items-center gap-3 border-b border-line px-4 sm:px-5 max-md:h-[calc(3.5rem+env(safe-area-inset-top))] max-md:gap-2 max-md:pt-[env(safe-area-inset-top)] max-md:pl-1.5">
872 <Button variant="ghost" size="icon-bar" onClick={onBack} aria-label="Back to conversations" className="rounded-full md:hidden">
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)873 <ChevronLeft size={22} />
The top of every page is one surface: the breadcrumb, search, spend, Ask g1t, bell and + share one height, corners and spacing, Ask g1t is a plain tinted button with the g1t mark drawn on whole pixels, a page's own header sits directly under the bar at the same left edge, and every section, conversations and open artifacts included, draws the same rounded panel. Tables written in chat render as tables, and the site's file, checkbox, radio, number and date controls are the design system's. Button, Separator, Breadcrumb, Table and FileDrop join the shared components; the chat guide says how tables work.874 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to875 <div className="flex min-w-0 grow items-center gap-2.5">
876 {channel.kind === "channel" ? (
877 <>
878 <h1 className="flex min-w-0 shrink-0 items-center gap-1 text-[0.9375rem] font-semibold">
Chat controls, public profiles, shadcn selects, and no Docs tab in a project879 {channel.private ? (
880 <Hint label="Private channel: only its members can find and read it">
881 <span className="flex">
882 <Lock size={15} className="text-faint" aria-label="Private channel" />
883 </span>
884 </Hint>
885 ) : (
886 <Hash size={16} className="text-faint" aria-label="Public channel" />
887 )}
Chat and workspace agents: channels, DMs and named agents you talk to888 <span className="truncate">{channel.name}</span>
889 </h1>
890 <StarButton starred={starred} onClick={onStar} />
891 {muted && (
892 <Hint label="Muted: no unread count or notifications">
893 <span className="text-faint">
894 <BellOff size={14} aria-label="Muted" />
895 </span>
896 </Hint>
897 )}
Chat controls, public profiles, shadcn selects, and no Docs tab in a project898 {channel.archived_at && (
899 <Badge tone="neutral" className="shrink-0">
900 <Archive size={11} />
901 Archived
902 </Badge>
903 )}
Chat and workspace agents: channels, DMs and named agents you talk to904 {channel.topic && (
905 <>
906 <span aria-hidden="true" className="hidden h-4 w-px shrink-0 bg-line sm:block" />
907 <p className="hidden min-w-0 truncate text-sm text-muted sm:block">{channel.topic}</p>
908 </>
909 )}
910 </>
911 ) : (
912 <>
913 <span className="flex shrink-0 -space-x-1.5">
914 {others.slice(0, 3).map((member) => (
915 <span key={`${member.kind}:${member.id}`} className="rounded-full ring-2 ring-bg">
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers916 <MemberAvatar member={member} size={26} presence={others.length === 1} />
Chat and workspace agents: channels, DMs and named agents you talk to917 </span>
918 ))}
919 </span>
920 <div className="min-w-0">
921 <h1 className="flex min-w-0 items-center gap-1.5 text-[0.9375rem] leading-tight font-semibold">
922 <span className="truncate">{others.map(shownName).join(", ") || "Just you"}</span>
923 {agentDm && <AgentPill />}
924 </h1>
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)925 {agentDm && (titleOf(agentDm) ?? agentDm.role) && (
926 <p className="truncate text-xs leading-tight text-muted">{titleOf(agentDm) ?? agentDm.role}</p>
927 )}
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.928 {mixed && <MixedChip agents={agentsAmong(others)} />}
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers929 {/* One person: their status under their name, as their card has it. */}
930 {!agentDm && others.length === 1 && others[0]!.kind === "user" && <PersonStatusLine person={{ id: others[0]!.id, username: others[0]!.name }} className="truncate text-xs leading-tight text-muted" />}
Chat and workspace agents: channels, DMs and named agents you talk to931 </div>
932 <StarButton starred={starred} onClick={onStar} />
933 </>
934 )}
935 </div>
936 {live !== "open" && (
937 <span role="status" className="hidden items-center gap-1.5 text-xs text-faint sm:flex">
938 <span className="size-1.5 animate-pulse rounded-full bg-warn" />
939 {live === "reconnecting" ? "Reconnecting…" : "Connecting…"}
940 </span>
941 )}
942 {channel.kind === "channel" && (
943 <Hint label={info ? "Hide details" : "Members and details"}>
The top of every page is one surface: the breadcrumb, search, spend, Ask g1t, bell and + share one height, corners and spacing, Ask g1t is a plain tinted button with the g1t mark drawn on whole pixels, a page's own header sits directly under the bar at the same left edge, and every section, conversations and open artifacts included, draws the same rounded panel. Tables written in chat render as tables, and the site's file, checkbox, radio, number and date controls are the design system's. Button, Separator, Breadcrumb, Table and FileDrop join the shared components; the chat guide says how tables work.944 <Button variant="outline" size="sm" onClick={onInfo} aria-pressed={info} aria-label="Members and details" className="px-2 text-xs text-muted tabular-nums aria-pressed:border-line-strong aria-pressed:bg-raised aria-pressed:text-fg max-md:h-11">
Chat and workspace agents: channels, DMs and named agents you talk to945 <UsersStack count={memberCount} />
The top of every page is one surface: the breadcrumb, search, spend, Ask g1t, bell and + share one height, corners and spacing, Ask g1t is a plain tinted button with the g1t mark drawn on whole pixels, a page's own header sits directly under the bar at the same left edge, and every section, conversations and open artifacts included, draws the same rounded panel. Tables written in chat render as tables, and the site's file, checkbox, radio, number and date controls are the design system's. Button, Separator, Breadcrumb, Table and FileDrop join the shared components; the chat guide says how tables work.946 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to947 </Hint>
948 )}
949 {channel.kind === "dm" && (
950 <Hint label={info ? "Hide details" : "Details"}>
The top of every page is one surface: the breadcrumb, search, spend, Ask g1t, bell and + share one height, corners and spacing, Ask g1t is a plain tinted button with the g1t mark drawn on whole pixels, a page's own header sits directly under the bar at the same left edge, and every section, conversations and open artifacts included, draws the same rounded panel. Tables written in chat render as tables, and the site's file, checkbox, radio, number and date controls are the design system's. Button, Separator, Breadcrumb, Table and FileDrop join the shared components; the chat guide says how tables work.951 <Button variant="ghost" size="icon-bar" onClick={onInfo} aria-pressed={info} aria-label="Details">
Chat and workspace agents: channels, DMs and named agents you talk to952 <PanelRight size={16} />
The top of every page is one surface: the breadcrumb, search, spend, Ask g1t, bell and + share one height, corners and spacing, Ask g1t is a plain tinted button with the g1t mark drawn on whole pixels, a page's own header sits directly under the bar at the same left edge, and every section, conversations and open artifacts included, draws the same rounded panel. Tables written in chat render as tables, and the site's file, checkbox, radio, number and date controls are the design system's. Button, Separator, Breadcrumb, Table and FileDrop join the shared components; the chat guide says how tables work.953 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to954 </Hint>
955 )}
956 </header>
957 );
958}
959
960function UsersStack({ count }: { count: number }) {
961 return (
962 <>
963 <Users size={14} />
964 {count}
965 </>
966 );
967}
968
969function StarButton({ starred, onClick }: { starred: boolean; onClick: () => void }) {
970 return (
971 <Hint label={starred ? "Unstar" : "Star: keep it at the top of the sidebar"}>
The top of every page is one surface: the breadcrumb, search, spend, Ask g1t, bell and + share one height, corners and spacing, Ask g1t is a plain tinted button with the g1t mark drawn on whole pixels, a page's own header sits directly under the bar at the same left edge, and every section, conversations and open artifacts included, draws the same rounded panel. Tables written in chat render as tables, and the site's file, checkbox, radio, number and date controls are the design system's. Button, Separator, Breadcrumb, Table and FileDrop join the shared components; the chat guide says how tables work.972 <Button
973 variant="ghost"
974 size="icon-sm"
Chat and workspace agents: channels, DMs and named agents you talk to975 aria-label={starred ? "Unstar" : "Star"}
976 aria-pressed={starred}
977 onClick={onClick}
The top of every page is one surface: the breadcrumb, search, spend, Ask g1t, bell and + share one height, corners and spacing, Ask g1t is a plain tinted button with the g1t mark drawn on whole pixels, a page's own header sits directly under the bar at the same left edge, and every section, conversations and open artifacts included, draws the same rounded panel. Tables written in chat render as tables, and the site's file, checkbox, radio, number and date controls are the design system's. Button, Separator, Breadcrumb, Table and FileDrop join the shared components; the chat guide says how tables work.978 className="text-faint aria-pressed:bg-transparent aria-pressed:text-warn aria-pressed:hover:bg-raised max-md:size-10"
Chat and workspace agents: channels, DMs and named agents you talk to979 >
980 <Star size={15} fill={starred ? "currentColor" : "none"} />
The top of every page is one surface: the breadcrumb, search, spend, Ask g1t, bell and + share one height, corners and spacing, Ask g1t is a plain tinted button with the g1t mark drawn on whole pixels, a page's own header sits directly under the bar at the same left edge, and every section, conversations and open artifacts included, draws the same rounded panel. Tables written in chat render as tables, and the site's file, checkbox, radio, number and date controls are the design system's. Button, Separator, Breadcrumb, Table and FileDrop join the shared components; the chat guide says how tables work.981 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to982 </Hint>
983 );
984}
985
986/** How a conversation begins, above its first message. */
987function ConversationStart({ data, others }: { data: Loaded; others: MemberProfile[] }) {
988 const channel = data.channel;
989 if (channel.kind === "dm") {
990 const agent = others.length === 1 && others[0]!.kind === "agent" ? others[0]! : null;
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.991 const mixed = isMixed(others);
Chat and workspace agents: channels, DMs and named agents you talk to992 return (
993 <div className="px-2 pb-6 sm:px-3">
994 <div className="flex -space-x-2">
995 {others.slice(0, 4).map((m) => (
996 <span key={m.id} className="rounded-full ring-4 ring-bg">
997 <MemberAvatar member={m} size={56} />
998 </span>
999 ))}
1000 </div>
1001 <h2 className="mt-3 flex items-center gap-2 text-xl font-semibold tracking-tight">
1002 {others.map(shownName).join(", ") || "Just you"}
1003 {agent && <AgentPill />}
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.1004 {mixed && (
1005 <Badge tone="accent" size="md" className="font-normal">
1006 with an agent
1007 </Badge>
1008 )}
Chat and workspace agents: channels, DMs and named agents you talk to1009 </h2>
1010 <p className="mt-1 max-w-xl text-sm text-muted">
1011 {agent
1012 ? `${agent.role ? `${agent.role}. ` : ""}Ask a question or give it a job: it answers here, and opens a task when the work needs one.`
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.1013 : mixed
1014 ? "A conversation between people and an agent. Everyone here, the agent too, reads every message."
1015 : "This is the start of your conversation."}
Chat and workspace agents: channels, DMs and named agents you talk to1016 </p>
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.1017 {mixed && <MixedNotice channelId={channel.id} others={others} />}
Chat and workspace agents: channels, DMs and named agents you talk to1018 </div>
1019 );
1020 }
1021 return (
1022 <div className="px-2 pb-6 sm:px-3">
1023 <span className="flex size-12 items-center justify-center rounded-xl bg-raised text-muted">
1024 {channel.private ? <Lock size={22} /> : <Hash size={24} />}
1025 </span>
1026 <h2 className="mt-3 text-xl font-semibold tracking-tight">Welcome to #{channel.name}</h2>
1027 <p className="mt-1 max-w-xl text-sm text-muted">{channel.topic ?? "This is the very beginning of the channel."}</p>
1028 </div>
1029 );
1030}
1031
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.1032/**
1033 * The chip a mixed direct message carries in its header, always: each agent
1034 * in it by face and handle, said to be an agent, so a person writing to a
1035 * colleague never forgets who else reads along.
1036 */
1037function MixedChip({ agents }: { agents: MemberProfile[] }) {
1038 const handles = agents.map((a) => `@${shownHandle(a)}`);
1039 const list = handles.length <= 2 ? handles.join(" and ") : `${handles.slice(0, -1).join(", ")} and ${handles[handles.length - 1]}`;
1040 return (
1041 <Badge tone="accent" size="md" className="mt-0.5 max-w-full gap-1.5 pl-1 font-normal">
1042 <span className="flex shrink-0 -space-x-1">
1043 {agents.slice(0, 3).map((a) => (
1044 <AgentAvatar key={a.id} agent={a} size={16} ring="var(--color-bg)" />
1045 ))}
1046 </span>
1047 <span className="truncate">
1048 {list}, {agents.length === 1 ? "agent" : "agents"}
1049 <span className="max-md:hidden">, {agents.length === 1 ? "is" : "are"} in this conversation</span>
1050 </span>
1051 </Badge>
1052 );
1053}
1054
1055const MIXED_SEEN_KEY = (channelId: string) => `g1t:chat-mixed-seen:${channelId}`;
1056
1057/**
1058 * The one-line notice above a mixed conversation's first message, the
1059 * first time a person opens it on this device: who the agents are and
1060 * that they read everything and can act on it. Dismissed, it stays away.
1061 */
1062function MixedNotice({ channelId, others }: { channelId: string; others: MemberProfile[] }) {
1063 const [seen, setSeen] = useState(true);
1064 useEffect(() => {
1065 try {
1066 setSeen(localStorage.getItem(MIXED_SEEN_KEY(channelId)) === "1");
1067 } catch {
1068 setSeen(false);
1069 }
1070 }, [channelId]);
1071 const dismiss = () => {
1072 setSeen(true);
1073 try {
1074 localStorage.setItem(MIXED_SEEN_KEY(channelId), "1");
1075 } catch {
1076 // Not kept: it shows again next time, which is fine.
1077 }
1078 };
1079 const words = mixedNotice(others);
1080 if (seen || !words) return null;
1081 return (
1082 <Card tone="bg" radius="lg" className="mt-4 flex max-w-xl items-center gap-2.5 py-2 pr-1.5 pl-3 text-sm text-fg-soft" role="note">
1083 <span className="flex shrink-0 -space-x-1">
1084 {agentsAmong(others).slice(0, 3).map((a) => (
1085 <AgentAvatar key={a.id} agent={a} size={20} ring="var(--color-bg)" />
1086 ))}
1087 </span>
1088 <span className="min-w-0 grow">{words}</span>
1089 <Hint label="Got it">
1090 <Button type="button" variant="ghost" size="icon-sm" aria-label="Dismiss" onClick={dismiss} className="shrink-0 text-faint">
1091 <X size={14} />
1092 </Button>
1093 </Hint>
1094 </Card>
1095 );
1096}
1097
Chat and workspace agents: channels, DMs and named agents you talk to1098function DayRule({ label }: { label: string }) {
1099 return (
1100 <div role="separator" aria-label={label} className="relative my-3 flex items-center justify-center">
1101 <span aria-hidden="true" className="absolute inset-x-2 top-1/2 h-px bg-line" />
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.1102 <Badge className="relative bg-bg px-3 py-0.5" suppressHydrationWarning>
Chat and workspace agents: channels, DMs and named agents you talk to1103 {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.1104 </Badge>
Chat and workspace agents: channels, DMs and named agents you talk to1105 </div>
1106 );
1107}
1108
1109/** One message: with its author's name and avatar when it starts a group. */
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)1110/**
1111 * Someone's profile beside the conversation, opened from their card: who
1112 * they are, their teams and local time, and what they said here lately.
1113 * The conversation stays where it was.
1114 */
1115function ProfilePanel({
1116 slug,
1117 username,
1118 messages,
1119 zone,
1120 context,
1121}: {
1122 slug: string;
1123 username: string;
1124 messages: ShownMessage[];
1125 zone: string | undefined;
1126 context: TextContext;
1127}) {
1128 const [card, setCard] = useState<PersonCard | null | undefined>(undefined);
1129 useEffect(() => {
1130 let live = true;
1131 setCard(undefined);
1132 void personCard(slug, username).then((value) => {
1133 if (live) setCard(value);
1134 });
1135 return () => {
1136 live = false;
1137 };
1138 }, [slug, username]);
1139 const theirs = messages
1140 .filter((m) => m.author.kind === "user" && m.author.name === username && !m.deleted_at && m.body)
1141 .slice(-5)
1142 .reverse();
1143 const time = card ? localTime(card.timezone, Date.now()) : null;
1144 return (
1145 <div className="min-h-0 grow overflow-y-auto p-5 [scrollbar-width:thin]">
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1146 <WithPresence person={{ username }} size={88}>
1147 <Avatar name={username} image={card?.avatar ?? null} size={88} />
1148 </WithPresence>
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar1149 <h3 className="mt-3 text-xl font-semibold tracking-tight">{card?.name?.trim() || (card ? shownUsername(card) : username)}</h3>
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)1150 <p className="font-mono text-sm text-muted">
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar1151 @{card ? shownUsername(card) : username}
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)1152 {card?.pronouns ? <span className="font-sans"> · {card.pronouns}</span> : null}
1153 </p>
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1154 <PresenceSummary person={{ username }} className="mt-3 text-sm text-muted" />
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)1155 {card?.bio && <p className="mt-3 text-sm leading-relaxed text-fg-soft">{card.bio}</p>}
1156 <dl className="mt-4 space-y-2 text-sm">
1157 {card && card.teams.length > 0 && (
1158 <div className="flex gap-3">
1159 <dt className="w-20 shrink-0 text-faint">Teams</dt>
1160 <dd className="text-fg-soft">{card.teams.map((team) => team.name).join(", ")}</dd>
1161 </div>
1162 )}
1163 {time && (
1164 <div className="flex gap-3">
1165 <dt className="w-20 shrink-0 text-faint">Local time</dt>
1166 <dd className="text-fg-soft" suppressHydrationWarning>
1167 {time}
1168 </dd>
1169 </div>
1170 )}
1171 {card?.location && (
1172 <div className="flex gap-3">
1173 <dt className="w-20 shrink-0 text-faint">Location</dt>
1174 <dd className="text-fg-soft">{card.location}</dd>
1175 </div>
1176 )}
1177 {card && card.workspaces.length > 0 && (
1178 <div className="flex gap-3">
1179 <dt className="w-20 shrink-0 text-faint">Workspaces</dt>
1180 <dd className="text-fg-soft">{card.workspaces.map((w) => w.name).join(", ")}</dd>
1181 </div>
1182 )}
1183 </dl>
1184 <h4 className="mt-6 mb-2 text-xs font-medium text-faint">Lately in this conversation</h4>
1185 {theirs.length === 0 ? (
1186 <p className="text-sm text-muted">Nothing here yet.</p>
1187 ) : (
1188 <ul className="space-y-3">
1189 {theirs.map((m) => (
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.1190 <Card asChild key={m.id} radius="lg" className="px-3 py-2.5">
1191 <li>
1192 <p className="mb-1 text-[0.6875rem] text-faint" suppressHydrationWarning>
1193 {clock(m.created_at, zone)}
1194 </p>
1195 <div className="line-clamp-4 text-sm">
1196 <MessageText body={m.body} context={context} />
1197 </div>
1198 </li>
1199 </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)1200 ))}
1201 </ul>
1202 )}
1203 <Link to={`/u/${username}`} className="mt-6 inline-block text-sm text-accent hover:underline">
1204 Open their full profile
1205 </Link>
1206 </div>
1207 );
1208}
1209
1210/** An agent's title ("QA Engineer"), when the chat service sends it with the member. */
1211function titleOf(member: MemberProfile): string | null {
1212 return (member as MemberProfile & { title?: string | null }).title ?? null;
1213}
1214
1215/** A long press (half a second, without moving) on a touch screen. */
1216function useLongPress(onLongPress?: () => void) {
1217 const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
1218 const cancel = () => {
1219 if (timer.current) clearTimeout(timer.current);
1220 timer.current = null;
1221 };
1222 if (!onLongPress) return {};
1223 return {
1224 onTouchStart: () => {
1225 cancel();
1226 timer.current = setTimeout(() => {
1227 timer.current = null;
1228 if (typeof navigator !== "undefined" && "vibrate" in navigator) navigator.vibrate?.(8);
1229 onLongPress();
1230 }, 480);
1231 },
1232 onTouchMove: cancel,
1233 onTouchEnd: cancel,
1234 onTouchCancel: cancel,
1235 onContextMenu: (event: React.MouseEvent) => {
1236 // A phone's own long-press menu (copy, look up) would cover ours.
1237 if (window.matchMedia("(pointer: coarse)").matches) event.preventDefault();
1238 },
1239 };
1240}
1241
1242/** A long-pressed message's actions, in a sheet from the bottom: reply, copy, and for your own, edit and delete. */
1243function MessageActions({
1244 message,
1245 mine,
1246 onClose,
1247 onThread,
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1248 onCopyLink,
1249 onWriteUp,
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)1250 onEdit,
1251 onDelete,
1252}: {
1253 message: ShownMessage | null;
1254 mine: boolean;
1255 onClose: () => void;
1256 onThread: (id: string) => void;
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1257 onCopyLink: (message: ShownMessage) => void;
1258 /** Absent where the person cannot post (not joined, archived). */
1259 onWriteUp?: (message: ShownMessage) => void;
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)1260 onEdit: (message: ShownMessage) => void;
1261 onDelete: (message: ShownMessage) => void;
1262}) {
1263 const [confirm, setConfirm] = useState(false);
1264 useEffect(() => setConfirm(false), [message]);
1265 const act = (run: () => void) => () => {
1266 run();
1267 onClose();
1268 };
1269 return (
1270 <BottomSheet open={message != null} onOpenChange={(open) => !open && onClose()} title="Message actions">
1271 {message && (
1272 <>
1273 <p className="mt-1 mb-2 line-clamp-2 px-3 text-sm text-muted">
1274 <span className="font-medium text-fg-soft">{shownName(message.author)}:</span> {message.body || message.card?.title}
1275 </p>
1276 {!message.pending && <QuickReactions messageId={message.id} onDone={onClose} />}
1277 {!message.thread_root && (
1278 <SheetRow icon={<MessageSquareText />} onClick={act(() => onThread(message.id))}>
1279 Reply in thread
1280 </SheetRow>
1281 )}
1282 <SheetRow icon={<Copy />} onClick={act(() => void navigator.clipboard?.writeText(message.body || message.card?.title || ""))}>
1283 Copy text
1284 </SheetRow>
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1285 {!message.pending && (
1286 <SheetRow icon={<Link2 />} onClick={act(() => onCopyLink(message))}>
1287 Copy link to thread
1288 </SheetRow>
1289 )}
1290 {!message.pending && onWriteUp && (
1291 <SheetRow icon={<FileText />} onClick={act(() => onWriteUp(message))}>
Artifacts is a mode in the rail, the shell and the phone's tab bar in place of Docs: its home lists everything you can open by day or as cards under All, Yours and Shared with you, with search, filters and tiles to start a doc (slides, designs and dashboards say they are coming soon), and its sidebar holds favorites, spaces, Private, Shared, projects' docs and the trash.1292 Write this up as an artifact
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1293 </SheetRow>
1294 )}
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)1295 {mine && message.kind === "text" && (
1296 <SheetRow icon={<Pencil />} onClick={act(() => onEdit(message))}>
1297 Edit
1298 </SheetRow>
1299 )}
1300 {mine &&
1301 (confirm ? (
1302 <SheetRow icon={<Trash2 className="text-danger" />} onClick={act(() => onDelete(message))}>
1303 <span className="text-danger">Delete for everyone</span>
1304 </SheetRow>
1305 ) : (
1306 <SheetRow icon={<Trash2 />} onClick={() => setConfirm(true)}>
1307 Delete
1308 </SheetRow>
1309 ))}
1310 </>
1311 )}
1312 </BottomSheet>
1313 );
1314}
1315
Chat and workspace agents: channels, DMs and named agents you talk to1316function MessageRow({
1317 message,
1318 head,
1319 zone,
1320 context,
1321 code,
1322 slug,
1323 onThread,
1324 onRetry,
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)1325 onLongPress,
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1326 onCopyLink,
1327 onWriteUp,
Chat and workspace agents: channels, DMs and named agents you talk to1328 threadOpen,
1329 inThread,
Merge main into Artifacts Phase 21330 onEdit,
1331 editor,
Chat and workspace agents: channels, DMs and named agents you talk to1332}: {
Merge main into Artifacts Phase 21333 /** Your own message: "Edit message" in the "⋯", and the composer in its place while editing. */
1334 onEdit?: () => void;
1335 editor?: ReactNode;
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)1336 /** A phone: a long press opens the message's actions. */
1337 onLongPress?: () => void;
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1338 /** The hover toolbar's "⋯": copy the link to its thread, write it up in Docs. */
1339 onCopyLink?: () => void;
1340 onWriteUp?: () => void;
Chat and workspace agents: channels, DMs and named agents you talk to1341 message: ShownMessage;
1342 head: boolean;
1343 zone: string | undefined;
1344 context: TextContext;
1345 code: boolean;
1346 slug: string;
1347 onThread?: () => void;
1348 onRetry: () => void;
1349 threadOpen?: boolean;
1350 inThread?: boolean;
1351}) {
1352 const author = message.author;
1353 const time = clock(message.created_at, zone);
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)1354 const press = useLongPress(onLongPress);
Chat and workspace agents: channels, DMs and named agents you talk to1355 return (
1356 <article
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)1357 {...press}
Chat and workspace agents: channels, DMs and named agents you talk to1358 aria-label={`${shownName(author)}, ${time}`}
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)1359 className={`group/message relative flex gap-3 rounded-lg px-2 transition-colors hover:bg-surface/70 sm:px-3 max-md:min-h-11 max-md:[-webkit-touch-callout:none] max-md:active:bg-surface/70 ${head ? "mt-3 pt-1 pb-1" : "py-0.5"} ${
Chat and workspace agents: channels, DMs and named agents you talk to1360 threadOpen ? "bg-accent/[0.06] hover:bg-accent/[0.08]" : ""
1361 }`}
1362 >
1363 <div className="w-9 shrink-0">
1364 {head ? (
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)1365 <MemberCard member={author} className="mt-0.5 block rounded-full" label={`${shownName(author)}'s card`}>
Chat and workspace agents: channels, DMs and named agents you talk to1366 <MemberAvatar member={author} size={36} />
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)1367 </MemberCard>
Chat and workspace agents: channels, DMs and named agents you talk to1368 ) : (
1369 <time
1370 dateTime={message.created_at}
1371 suppressHydrationWarning
1372 className="mt-1 block text-right text-[0.625rem] leading-5 text-faint tabular-nums opacity-0 group-hover/message:opacity-100"
1373 >
1374 {time.replace(/\s?[AP]M$/, "")}
1375 </time>
1376 )}
1377 </div>
1378 <div className="min-w-0 grow">
1379 {head && (
1380 <div className="flex items-baseline gap-2">
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)1381 <MemberCard member={author} className="truncate text-[0.9375rem] font-semibold text-fg hover:underline">
Chat and workspace agents: channels, DMs and named agents you talk to1382 {shownName(author)}
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)1383 </MemberCard>
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1384 {author.kind === "user" && <PersonStatusEmoji person={{ id: author.id, username: author.name }} size={14} className="self-center" />}
Chat and workspace agents: channels, DMs and named agents you talk to1385 {author.kind === "agent" && <AgentPill className="self-center" />}
1386 <time dateTime={message.created_at} suppressHydrationWarning className="shrink-0 text-xs text-faint tabular-nums">
1387 {time}
1388 </time>
1389 </div>
1390 )}
1391 <div className={message.pending && !message.failed ? "opacity-60" : undefined}>
Merge main into Artifacts Phase 21392 {editor ? <div className="py-1">{editor}</div> : message.body && <MessageText body={message.body} context={context} />}
1393 {!editor && message.body && !message.pending && <FolioUnfurls slug={slug} body={message.body} />}
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar1394 {message.card && (
1395 <CardBox
1396 card={message.card}
1397 slug={slug}
1398 code={code}
1399 channelId={message.channel_id}
1400 messageId={message.id}
1401 inert={!!message.pending || !!message.deleted_at}
1402 />
1403 )}
Chat and workspace agents: channels, DMs and named agents you talk to1404 {message.edited_at && <span className="text-[0.6875rem] text-faint"> (edited)</span>}
1405 </div>
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)1406 {!message.pending && !message.deleted_at && <ReactionBar messageId={message.id} reactions={message.reactions} />}
Chat and workspace agents: channels, DMs and named agents you talk to1407 {message.failed && (
1408 <p className="mt-0.5 text-xs text-danger">
1409 Not sent.{" "}
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.1410 <Button type="button" onClick={onRetry} variant="link" size="inline" className="text-inherit underline underline-offset-2 hover:text-fg">
Chat and workspace agents: channels, DMs and named agents you talk to1411 Try again
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.1412 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to1413 </p>
1414 )}
1415 {!inThread && message.reply_count > 0 && onThread && (
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.1416 <Button
Chat and workspace agents: channels, DMs and named agents you talk to1417 type="button"
1418 onClick={onThread}
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.1419 variant="ghost"
1420 size="inline"
1421 className="mt-1 -ml-1 flex gap-2 px-1 py-0.5 text-[0.8125rem] text-accent"
Chat and workspace agents: channels, DMs and named agents you talk to1422 >
1423 <MessageSquareText size={14} />
1424 {message.reply_count} {message.reply_count === 1 ? "reply" : "replies"}
1425 {message.last_reply_at && (
1426 <span className="font-normal text-faint" suppressHydrationWarning>
1427 Last reply {clock(message.last_reply_at, zone)}
1428 </span>
1429 )}
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.1430 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to1431 )}
1432 </div>
1433 {!inThread && onThread && !message.pending && (
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.1434 <Card radius="lg" className="absolute -top-3 right-3 hidden p-0.5 shadow-lg shadow-black/30 group-hover/message:flex group-focus-within/message:flex has-[[data-state=open]]:flex">
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)1435 <AddReaction messageId={message.id} />
Chat and workspace agents: channels, DMs and named agents you talk to1436 <Hint label="Reply in thread">
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.1437 <Button
Chat and workspace agents: channels, DMs and named agents you talk to1438 type="button"
1439 aria-label="Reply in thread"
1440 onClick={onThread}
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.1441 variant="ghost"
1442 size="icon-xs"
Chat and workspace agents: channels, DMs and named agents you talk to1443 >
1444 <MessageSquareText 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.1445 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to1446 </Hint>
Merge main into Artifacts Phase 21447 {onCopyLink && <MoreActions onCopyLink={onCopyLink} onWriteUp={onWriteUp} onEdit={onEdit} />}
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.1448 </Card>
Chat and workspace agents: channels, DMs and named agents you talk to1449 )}
1450 </article>
1451 );
1452}
1453
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1454/** A message's "⋯" in its hover toolbar. */
Merge main into Artifacts Phase 21455function MoreActions({ onCopyLink, onWriteUp, onEdit }: { onCopyLink: () => void; onWriteUp?: () => void; onEdit?: () => void }) {
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1456 return (
1457 <DropdownMenu>
1458 <Hint label="More actions">
1459 <DropdownMenuTrigger
1460 aria-label="More actions"
1461 className="flex size-7 items-center justify-center rounded-md text-muted outline-none hover:bg-raised hover:text-fg focus-visible:ring-2 focus-visible:ring-accent/50"
1462 >
1463 <Ellipsis size={15} />
1464 </DropdownMenuTrigger>
1465 </Hint>
1466 <DropdownMenuContent align="end">
Merge main into Artifacts Phase 21467 {onEdit && (
1468 <DropdownMenuItem onSelect={onEdit}>
1469 <Pencil />
1470 Edit message
1471 </DropdownMenuItem>
1472 )}
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1473 <ThreadMenuItems onCopyLink={onCopyLink} onWriteUp={onWriteUp} />
1474 </DropdownMenuContent>
1475 </DropdownMenu>
1476 );
1477}
1478
1479function ThreadMenuItems({ onCopyLink, onWriteUp }: { onCopyLink: () => void; onWriteUp?: () => void }) {
1480 return (
1481 <>
1482 <DropdownMenuItem onSelect={onCopyLink}>
1483 <Link2 />
1484 Copy link to thread
1485 </DropdownMenuItem>
1486 {onWriteUp && (
1487 <DropdownMenuItem onSelect={onWriteUp}>
1488 <FileText />
Artifacts is a mode in the rail, the shell and the phone's tab bar in place of Docs: its home lists everything you can open by day or as cards under All, Yours and Shared with you, with search, filters and tiles to start a doc (slides, designs and dashboards say they are coming soon), and its sidebar holds favorites, spaces, Private, Shared, projects' docs and the trash.1489 Write this up as an artifact
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1490 </DropdownMenuItem>
1491 )}
1492 </>
1493 );
1494}
1495
1496/** The thread panel's "⋯", in its header. */
1497function ThreadMenu({ onCopyLink, onWriteUp }: { onCopyLink: () => void; onWriteUp?: () => void }) {
1498 return (
1499 <DropdownMenu>
1500 <Hint label="Thread actions">
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.1501 <DropdownMenuTrigger asChild>
1502 <Button variant="ghost" size="icon-lg" aria-label="Thread actions" className="rounded-full active:bg-raised lg:size-7 lg:rounded-md">
1503 <Ellipsis size={16} />
1504 </Button>
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1505 </DropdownMenuTrigger>
1506 </Hint>
1507 <DropdownMenuContent align="end">
1508 <ThreadMenuItems onCopyLink={onCopyLink} onWriteUp={onWriteUp} />
1509 </DropdownMenuContent>
1510 </DropdownMenu>
1511 );
1512}
1513
In a group message with several agents, a message that names no agent goes to the one you are talking with: the agents that answered you last, else the ones you last mentioned, else, before anyone has been addressed, all of them; a message that mentions agents still goes only to those, and a conversation with one agent is as it was. Each agent is also told, when several are here, who you are talking with, so one that is woken alongside answers only what is clearly for it and does not redo a colleague's work. And an agent shows as typing for as long as it works, not for eight seconds: its desk says so every five seconds until the answer is posted, and after ten seconds the line reads that it is still working on it. The chat and agents guides say so.1514/**
1515 * "reviewer is typing…", for whoever is; "Mike is still working on it…"
1516 * for an agent that has been at a reply a while (`typingLabel`). The line
1517 * redraws itself the moment the first of them crosses that mark.
1518 */
1519function TypingLine({ typers }: { typers: Typer[] }) {
1520 const [, redraw] = useState(0);
1521 const now = Date.now();
1522 useEffect(() => {
1523 const waits = typers.map((t) => t.since + WORKING_AFTER_MS - now).filter((wait) => wait > 0);
1524 if (!waits.length) return;
1525 const timer = setTimeout(() => redraw((n) => n + 1), Math.min(...waits));
1526 return () => clearTimeout(timer);
1527 }, [typers, now]);
1528 const label = typingLabel(typers, now);
Chat and workspace agents: channels, DMs and named agents you talk to1529 return (
1530 <div aria-live="polite" className="flex h-6 items-center gap-1.5 px-1 text-xs text-muted">
In a group message with several agents, a message that names no agent goes to the one you are talking with: the agents that answered you last, else the ones you last mentioned, else, before anyone has been addressed, all of them; a message that mentions agents still goes only to those, and a conversation with one agent is as it was. Each agent is also told, when several are here, who you are talking with, so one that is woken alongside answers only what is clearly for it and does not redo a colleague's work. And an agent shows as typing for as long as it works, not for eight seconds: its desk says so every five seconds until the answer is posted, and after ten seconds the line reads that it is still working on it. The chat and agents guides say so.1531 {label && (
Chat and workspace agents: channels, DMs and named agents you talk to1532 <>
1533 <span aria-hidden="true" className="flex gap-0.5">
1534 {[0, 1, 2].map((i) => (
1535 <span key={i} className="size-1 animate-bounce rounded-full bg-accent" style={{ animationDelay: `${i * 140}ms` }} />
1536 ))}
1537 </span>
1538 <span>
In a group message with several agents, a message that names no agent goes to the one you are talking with: the agents that answered you last, else the ones you last mentioned, else, before anyone has been addressed, all of them; a message that mentions agents still goes only to those, and a conversation with one agent is as it was. Each agent is also told, when several are here, who you are talking with, so one that is woken alongside answers only what is clearly for it and does not redo a colleague's work. And an agent shows as typing for as long as it works, not for eight seconds: its desk says so every five seconds until the answer is posted, and after ten seconds the line reads that it is still working on it. The chat and agents guides say so.1539 <span className="font-medium text-fg-soft">{label.who}</span> {label.doing}…
Chat and workspace agents: channels, DMs and named agents you talk to1540 </span>
1541 </>
1542 )}
1543 </div>
1544 );
1545}
1546
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)1547/**
1548 * A panel beside the conversation: a thread, or the details. On a phone it
1549 * is a screen of its own, pushed over the conversation, as tall as what
1550 * the keyboard leaves.
1551 */
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1552function SidePanel({ title, subtitle, onClose, menu, children }: { title: string; subtitle?: string; onClose: () => void; menu?: ReactNode; children: ReactNode }) {
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)1553 const swipe = useSwipeBack(onClose);
Chat and workspace agents: channels, DMs and named agents you talk to1554 return (
1555 <aside
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)1556 {...swipe}
Chat and workspace agents: channels, DMs and named agents you talk to1557 aria-label={title}
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)1558 className="fixed inset-0 z-40 flex flex-col bg-bg lg:static lg:z-auto lg:w-[22rem] lg:shrink-0 lg:border-l lg:border-line xl:w-[24rem] max-md:top-(--vv-top,0px) max-md:bottom-auto max-md:h-(--vv-height,100dvh) max-md:pb-[env(safe-area-inset-bottom)]"
Chat and workspace agents: channels, DMs and named agents you talk to1559 >
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)1560 <div className="flex h-14 shrink-0 items-center gap-2 border-b border-line px-4 max-md:h-[calc(3.5rem+env(safe-area-inset-top))] max-md:pt-[env(safe-area-inset-top)] max-md:pl-1.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.1561 <Button type="button" onClick={onClose} aria-label="Back" variant="ghost" size="icon-lg" className="rounded-full active:bg-raised lg:hidden">
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)1562 <ChevronLeft 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.1563 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to1564 <h2 className="text-[0.9375rem] font-semibold">{title}</h2>
1565 {subtitle && <span className="truncate text-sm text-faint">{subtitle}</span>}
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1566 <div className="ml-auto flex shrink-0 items-center gap-1">
1567 {menu}
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.1568 <Button type="button" onClick={onClose} aria-label="Close" variant="ghost" size="inline" className="hidden p-1 text-faint lg:block">
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1569 <X size={16} />
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.1570 </Button>
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1571 </div>
Chat and workspace agents: channels, DMs and named agents you talk to1572 </div>
1573 <div className="flex min-h-0 grow flex-col">{children}</div>
1574 </aside>
1575 );
1576}
1577
1578function ThreadPanel({
1579 root,
1580 replies,
1581 zone,
1582 context,
1583 code,
1584 slug,
1585 people,
1586 joined,
1587 onSend,
1588 onRetry,
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1589 onTyping,
Chat and workspace agents: channels, DMs and named agents you talk to1590 draftKey,
1591}: {
1592 root: ShownMessage | null;
1593 replies: ShownMessage[] | null;
1594 zone: string | undefined;
1595 context: TextContext;
1596 code: boolean;
1597 slug: string;
1598 people: Mentionable[];
1599 joined: boolean;
1600 onSend: (body: string) => void;
1601 onRetry: (message: ShownMessage) => void;
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1602 /** Says "is typing" to the others in the channel, as the main composer does. */
1603 onTyping: () => void;
Chat and workspace agents: channels, DMs and named agents you talk to1604 draftKey: string;
1605}) {
1606 const rows = useMemo(() => timeline(replies ?? [], new Date(), zone).filter((row) => row.kind === "message"), [replies, zone]);
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)1607 // The newest reply stays in view as replies arrive and as the keyboard opens, unless the reader scrolled up.
1608 const scroller = useRef<HTMLDivElement>(null);
1609 const stick = useRef(true);
1610 useKeepAtBottom(scroller, stick);
Chat and workspace agents: channels, DMs and named agents you talk to1611 return (
1612 <>
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)1613 <div
1614 ref={scroller}
1615 onScroll={(event) => {
1616 stick.current = atBottom(event.currentTarget);
1617 }}
1618 className="min-h-0 grow overflow-y-auto overscroll-contain px-1 py-3 [scrollbar-width:thin]"
1619 >
1620 {/* One box for what scrolls, so its growing is seen (./stick.ts). */}
1621 <div>
Chat and workspace agents: channels, DMs and named agents you talk to1622 {root ? (
1623 <MessageRow message={root} head zone={zone} context={context} code={code} slug={slug} onRetry={() => onRetry(root)} inThread />
1624 ) : (
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1625 replies != null && <p className="px-4 py-2 text-sm text-faint">The message this thread is under is further back.</p>
Chat and workspace agents: channels, DMs and named agents you talk to1626 )}
1627 <div className="my-2 flex items-center gap-3 px-4 text-xs text-faint">
1628 {replies == null ? "Loading replies…" : `${replies.filter((r) => !r.deleted_at).length} ${replies.length === 1 ? "reply" : "replies"}`}
1629 <span className="h-px grow bg-line" />
1630 </div>
1631 {rows.map((row) =>
1632 row.kind === "message" ? (
1633 <MessageRow
1634 key={row.key}
1635 message={row.message}
1636 head={row.head}
1637 zone={zone}
1638 context={context}
1639 code={code}
1640 slug={slug}
1641 onRetry={() => onRetry(row.message)}
1642 inThread
1643 />
1644 ) : null,
1645 )}
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)1646 </div>
Chat and workspace agents: channels, DMs and named agents you talk to1647 </div>
1648 {joined && (
1649 <div className="shrink-0 p-3">
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1650 <Composer draftKey={draftKey} placeholder="Reply…" people={people} onSend={onSend} onTyping={onTyping} compact autoFocus />
1651 </div>
1652 )}
1653 </>
1654 );
1655}
1656
1657/**
1658 * A channel's name, topic and visibility, and changing them: any member
1659 * edits the topic; renaming and archiving are for whoever the workspace's
1660 * chat settings allow (`canManage`). #general keeps its name and stays.
1661 */
1662function ChannelAbout({
1663 channel,
1664 joined,
1665 canManage,
1666 onUpdate,
1667}: {
1668 channel: Channel;
1669 joined: boolean;
1670 canManage: boolean;
1671 onUpdate: (change: ChannelChange) => Promise<Result<unknown>>;
1672}) {
1673 const [editing, setEditing] = useState<"name" | "topic" | null>(null);
1674 const [value, setValue] = useState("");
1675 const [error, setError] = useState<string | null>(null);
1676 const [busy, setBusy] = useState(false);
1677 const [confirm, setConfirm] = useState(false);
1678 const general = channel.name === "general";
1679 const archived = !!channel.archived_at;
1680 const mayRename = canManage && !general && !archived;
1681 const mayTopic = joined && !archived;
1682 const save = async (change: ChannelChange) => {
1683 setBusy(true);
1684 setError(null);
1685 const done = await onUpdate(change);
1686 setBusy(false);
1687 if (!done.ok) return setError(done.error.message);
1688 setEditing(null);
1689 setConfirm(false);
1690 };
1691 const start = (field: "name" | "topic") => {
1692 setEditing(field);
1693 setValue(field === "name" ? (channel.name ?? "") : (channel.topic ?? ""));
1694 setError(null);
1695 };
1696 const edit = (field: "name" | "topic") => (
1697 <form
1698 className="mt-1.5 space-y-2"
1699 onSubmit={(event) => {
1700 event.preventDefault();
1701 void save(field === "name" ? { name: value } : { topic: value });
1702 }}
1703 >
1704 <span className="flex h-9 items-center gap-1.5 rounded-md border border-line bg-bg px-2.5 focus-within:border-accent-dim">
1705 {field === "name" && (channel.private ? <Lock size={13} className="text-faint" /> : <Hash size={13} className="text-faint" />)}
1706 <input
1707 autoFocus
1708 value={value}
1709 onChange={(event) => setValue(event.target.value)}
1710 onKeyDown={(event) => event.key === "Escape" && setEditing(null)}
1711 maxLength={field === "name" ? 80 : 250}
1712 placeholder={field === "name" ? "channel-name" : "What this channel is for"}
1713 aria-label={field === "name" ? "Channel name" : "Topic"}
1714 autoComplete="off"
1715 data-1p-ignore
1716 className="min-w-0 grow bg-transparent text-sm outline-none placeholder:text-faint"
1717 />
1718 </span>
1719 <div className="flex justify-end gap-1.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.1720 <Button type="button" onClick={() => setEditing(null)} variant="ghost" size="xs" className="px-2.5 font-normal">
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1721 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.1722 </Button>
1723 <Button type="submit" disabled={busy} variant="accent" size="xs" className="px-2.5">
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1724 {busy ? "Saving…" : "Save"}
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.1725 </Button>
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1726 </div>
1727 </form>
1728 );
1729 const editButton = (field: "name" | "topic", label: string) => (
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.1730 <Button
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1731 type="button"
1732 onClick={() => start(field)}
1733 aria-label={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.1734 variant="ghost"
1735 size="inline"
1736 className="rounded px-1.5 py-0.5 text-xs opacity-0 transition-opacity group-hover/about:opacity-100 focus-visible:opacity-100 [@media(hover:none)]:opacity-100 font-normal"
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1737 >
1738 Edit
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.1739 </Button>
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1740 );
1741 return (
1742 <>
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.1743 <Card asChild className="group/about p-3.5">
1744 <section>
1745 <div className="flex items-center justify-between">
1746 <h3 className="text-xs font-medium text-faint">Name</h3>
1747 {mayRename && editing !== "name" && editButton("name", "Rename channel")}
1748 </div>
1749 {editing === "name" ? edit("name") : <p className="mt-1 text-sm text-fg-soft">#{channel.name}</p>}
1750 <div className="mt-3 flex items-center justify-between">
1751 <h3 className="text-xs font-medium text-faint">Topic</h3>
1752 {mayTopic && editing !== "topic" && editButton("topic", "Edit topic")}
1753 </div>
1754 {editing === "topic" ? edit("topic") : <p className="mt-1 text-sm text-fg-soft">{channel.topic || "No topic yet."}</p>}
1755 <h3 className="mt-3 text-xs font-medium text-faint">Visibility</h3>
1756 <p className="mt-1 flex items-center gap-1.5 text-sm text-fg-soft">
1757 {channel.private ? <Lock size={13} /> : <Hash size={13} />}
1758 {channel.private ? "Private: only its members can find and read it" : "Public: anyone in the workspace can read and join"}
1759 </p>
1760 {error && <p className="mt-2 text-xs text-danger">{error}</p>}
1761 </section>
1762 </Card>
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1763 {canManage && !general && (
1764 <button
1765 type="button"
1766 onClick={() => (archived ? void save({ archived: false }) : setConfirm(true))}
1767 disabled={busy}
1768 className="mt-2 flex h-8 w-full items-center justify-center gap-2 rounded-md text-[0.8125rem] text-muted transition-colors hover:bg-raised hover:text-fg disabled:opacity-50"
1769 >
1770 {archived ? <ArchiveRestore size={14} /> : <Archive size={14} />}
1771 {archived ? "Unarchive channel" : "Archive channel"}
1772 </button>
Chat and workspace agents: channels, DMs and named agents you talk to1773 )}
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1774 <AlertDialog open={confirm} onOpenChange={setConfirm}>
1775 <AlertDialogContent>
1776 <AlertDialogHeader>
1777 <AlertDialogTitle>Archive #{channel.name}?</AlertDialogTitle>
1778 <AlertDialogDescription>
1779 It leaves everyone&apos;s sidebar and nobody can post in it. Its history stays readable, and it can be unarchived from Browse
1780 channels, Archived.
1781 </AlertDialogDescription>
1782 </AlertDialogHeader>
1783 <AlertDialogFooter>
1784 <AlertDialogCancel>Cancel</AlertDialogCancel>
1785 <AlertDialogAction
1786 onClick={(event) => {
1787 event.preventDefault();
1788 void save({ archived: true });
1789 }}
1790 disabled={busy}
1791 >
1792 {busy ? "Archiving…" : "Archive"}
1793 </AlertDialogAction>
1794 </AlertDialogFooter>
1795 </AlertDialogContent>
1796 </AlertDialog>
Chat and workspace agents: channels, DMs and named agents you talk to1797 </>
1798 );
1799}
1800
1801function InfoPanel({
1802 data,
1803 members,
1804 slug,
1805 people,
1806 onAdded,
1807 joined,
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1808 canManage,
1809 onUpdate,
Chat and workspace agents: channels, DMs and named agents you talk to1810}: {
1811 data: Loaded;
1812 members: ChannelMember[];
1813 slug: string;
1814 people: Mentionable[];
1815 onAdded: (member: ChannelMember) => void;
1816 joined: boolean;
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1817 /** Whether the viewer may rename, archive and unarchive it. */
1818 canManage: boolean;
1819 onUpdate: (change: ChannelChange) => Promise<Result<unknown>>;
Chat and workspace agents: channels, DMs and named agents you talk to1820}) {
1821 const channel = data.channel;
1822 const agents = members.filter((m) => m.member.kind === "agent");
1823 const humans = members.filter((m) => m.member.kind === "user");
1824 return (
1825 <div className="min-h-0 grow overflow-y-auto p-4 [scrollbar-width:thin]">
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1826 {channel.kind === "channel" && <ChannelAbout channel={channel} joined={joined} canManage={canManage} onUpdate={onUpdate} />}
Chat and workspace agents: channels, DMs and named agents you talk to1827 {agents.length > 0 && (
1828 <section className="mt-5">
1829 <h3 className="mb-2 px-1 text-xs font-medium text-faint">Agents · {agents.length}</h3>
1830 <div className="flex flex-wrap gap-1.5">
1831 {agents.map(({ member }) => (
1832 <Hint key={member.id} label={member.role ?? undefined}>
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)1833 <MemberCard
1834 member={member}
Chat and workspace agents: channels, DMs and named agents you talk to1835 className="flex h-7 items-center gap-1.5 rounded-full border border-accent/25 bg-accent/10 pr-2.5 pl-1 text-[0.8125rem] font-medium text-accent transition-colors hover:bg-accent/15"
1836 >
1837 <MemberAvatar member={member} size={20} />
1838 {shownName(member)}
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)1839 </MemberCard>
Chat and workspace agents: channels, DMs and named agents you talk to1840 </Hint>
1841 ))}
1842 </div>
1843 </section>
1844 )}
1845 <section className="mt-5">
1846 <div className="mb-1 flex items-center justify-between px-1">
1847 <h3 className="text-xs font-medium text-faint">People · {humans.length}</h3>
1848 {joined && channel.kind === "channel" && <AddMember slug={slug} channelId={channel.id} people={people} members={members} onAdded={onAdded} />}
1849 </div>
1850 <ul className="space-y-px">
1851 {humans.map(({ member, role }) => (
1852 <li key={member.id}>
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)1853 <MemberCard member={member} className="flex w-full items-center gap-2.5 rounded-md px-1 py-1.5 transition-colors hover:bg-raised/60 max-md:min-h-11">
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1854 <MemberAvatar member={member} size={26} presence />
Chat and workspace agents: channels, DMs and named agents you talk to1855 <span className="min-w-0 grow truncate text-sm">
1856 {shownName(member)}
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar1857 {shownName(member) !== shownHandle(member) && <span className="ml-1.5 text-faint">@{shownHandle(member)}</span>}
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1858 <PersonStatusEmoji person={{ id: member.id, username: member.name }} size={13} className="ml-1.5 align-[-2px]" inert />
Chat and workspace agents: channels, DMs and named agents you talk to1859 </span>
1860 {role === "owner" && <span className="text-[0.6875rem] text-faint">Owner</span>}
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)1861 </MemberCard>
Chat and workspace agents: channels, DMs and named agents you talk to1862 </li>
1863 ))}
1864 </ul>
1865 </section>
1866 </div>
1867 );
1868}
1869
1870/** Adding a person or an agent to the channel. An invite lets them read; it never grants write. */
1871function AddMember({
1872 slug,
1873 channelId,
1874 people,
1875 members,
1876 onAdded,
1877}: {
1878 slug: string;
1879 channelId: string;
1880 people: Mentionable[];
1881 members: ChannelMember[];
1882 onAdded: (member: ChannelMember) => void;
1883}) {
1884 const [open, setOpen] = useState(false);
1885 const [query, setQuery] = useState("");
1886 const [error, setError] = useState<string | null>(null);
1887 const send = useChatSend(slug);
1888 const chatData = useChatData();
1889 const inside = new Set(members.map((m) => `${m.member.kind}:${m.member.name.toLowerCase()}`));
1890 const q = query.trim().toLowerCase().replace(/^@/, "");
1891 const options = people
1892 .filter((p) => !inside.has(`${p.kind}:${p.name.toLowerCase()}`))
1893 .filter((p) => !q || p.name.toLowerCase().includes(q) || p.display_name.toLowerCase().includes(q))
1894 .slice(0, 8);
1895 const add = async (person: Mentionable) => {
1896 const agent = person.kind === "agent" ? chatData?.agents.find((a) => a.handle === person.name) : null;
1897 const key = agent ? `agent:${agent.id}` : `user:${person.name}`;
1898 setError(null);
1899 const done = await send({ intent: "invite", channel_id: channelId, member: key });
1900 if (!done.ok) return setError(done.error.message);
1901 onAdded({
1902 channel_id: channelId,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar1903 member: { kind: person.kind, id: agent?.id ?? person.name, name: person.name, display_username: person.display_username ?? null, display_name: person.display_name, avatar: person.avatar, role: person.role ?? null },
Chat and workspace agents: channels, DMs and named agents you talk to1904 role: "member",
1905 starred: false,
1906 muted: false,
1907 last_read_id: null,
1908 joined_at: new Date().toISOString(),
1909 });
1910 setOpen(false);
1911 };
1912 return (
1913 <Popover open={open} onOpenChange={setOpen}>
1914 <PopoverTrigger asChild>
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.1915 <Button type="button" variant="ghost" size="inline" className="flex gap-1 px-1.5 py-0.5 text-xs font-normal">
Chat and workspace agents: channels, DMs and named agents you talk to1916 <UserPlus size={13} />
1917 Add
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.1918 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to1919 </PopoverTrigger>
1920 <PopoverContent align="end" className="w-72 p-1.5">
1921 <input
1922 autoFocus
1923 value={query}
1924 onChange={(event) => setQuery(event.target.value)}
1925 placeholder="Add a person or an agent"
1926 aria-label="Add a person or an agent"
1927 autoComplete="off"
1928 data-1p-ignore
1929 className="mb-1 h-8 w-full rounded-md border border-line bg-bg px-2.5 text-sm outline-none placeholder:text-faint focus:border-accent-dim"
1930 />
1931 {options.length === 0 && <p className="px-2 py-3 text-xs text-faint">Everyone who matches is already here.</p>}
1932 {options.map((person) => (
1933 <button
1934 key={`${person.kind}:${person.name}`}
1935 type="button"
1936 onClick={() => void add(person)}
1937 className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm hover:bg-line"
1938 >
1939 <MemberAvatar member={person} size={20} />
1940 <span className="min-w-0 grow truncate">{person.display_name}</span>
1941 {person.kind === "agent" && <AgentPill />}
1942 </button>
1943 ))}
1944 {error && <p className="px-2 pt-1 text-xs text-danger">{error}</p>}
1945 <p className="border-t border-line px-2 pt-1.5 pb-0.5 text-[0.6875rem] leading-snug text-faint">
1946 Adding an agent lets it read this channel. It never grants it write access.
1947 </p>
1948 </PopoverContent>
1949 </Popover>
1950 );
1951}

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