Skip to content
1,913 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";
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.68import { type Mentionable, type ShownMessage, agentsAmong, channelPath, dmPlaceholder, isMixed, mentionNames, mergeMessages, mixedNotice, shownHandle, shownName, timeline } from "../../lib/chat";
Chat and workspace agents: channels, DMs and named agents you talk to69import { 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)70import { conversationCache } from "./conversation-cache";
71// Reactions and the workspace's own emoji (components/emoji).
72import { EmojiProvider, useCustomEmoji } from "../emoji/context";
73import { AddReaction, QuickReactions, ReactionBar } from "../emoji/reaction-bar";
74import { useAddresses } from "../../lib/addresses";
75import { 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.76import { dndNow } from "../../lib/notify-client";
77import { chatMessageArrived, messageSent, viewingConversation } from "../../lib/sound-events";
Chat and workspace agents: channels, DMs and named agents you talk to78import type { ChannelData } from "../../routes/workspace/chat/channel";
79
80type Loaded = Extract<ChannelData, { unavailable: false }>;
81
82/** True once the page has hydrated: times and days are the reader's own from then on. */
83function useHydrated(): boolean {
84 const [hydrated, setHydrated] = useState(false);
85 useEffect(() => setHydrated(true), []);
86 return hydrated;
87}
88
89function clock(at: string, zone: string | undefined): string {
90 return new Date(at).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", timeZone: zone });
91}
92
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.93/** Whether the viewer uses Code in this workspace. */
Chat and workspace agents: channels, DMs and named agents you talk to94export function useCodeAccess(slug: string): boolean {
95 const root = useRouteLoaderData("root") as { user?: { workspaces?: { slug: string; code_access?: boolean }[] } | null } | undefined;
96 return hasCodeAccess(root?.user?.workspaces?.find((m) => m.slug === slug) ?? null);
97}
98
99/** The project a channel of the same name is about, for a bare `#123`. */
100function useProjectNamed(name: string | null): string | null {
101 const root = useRouteLoaderData("root") as { shell?: { repos?: { name: string }[] } } | undefined;
102 if (!name) return null;
103 return root?.shell?.repos?.some((repo) => repo.name === name) ? name : null;
104}
105
106function newClientId(): string {
107 return `tmp-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
108}
109
110/** A conversation: its header, its messages, the box to write in, and its thread and details beside it. */
111export function ChannelView({ data }: { data: Loaded }) {
112 const chatData = useChatData();
113 const slug = chatData?.slug ?? "";
114 const me = chatData?.me ?? null;
115 const send = useChatSend(slug);
116 const { sidebar, refresh } = useChatSidebar();
117 const hydrated = useHydrated();
118 const zone = hydrated ? undefined : "UTC";
119 const code = useCodeAccess(slug);
120 const project = useProjectNamed(data.channel.kind === "channel" ? data.channel.name : null);
121 const [params, setParams] = useSearchParams();
122 const threadId = params.get("thread");
123
124 const [messages, setMessages] = useState<ShownMessage[]>(() => mergeMessages([], data.messages));
125 const [older, setOlder] = useState<string | null>(data.older);
126 const [loadingOlder, setLoadingOlder] = useState(false);
127 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 thread128 // The message the open thread is under, as its page says, for when it is
129 // further back than the conversation has loaded (a long-running session's card).
130 const [threadRoot, setThreadRoot] = useState<ShownMessage | null>(null);
Chat and workspace agents: channels, DMs and named agents you talk to131 const [typing, setTyping] = useState<Map<string, { member: MemberProfile; until: number }>>(new Map());
132 const [joined, setJoined] = useState(data.joined);
133 const [members, setMembers] = useState<ChannelMember[]>(data.members);
Chat controls, public profiles, shadcn selects, and no Docs tab in a project134 // The channel as it is now: renamed, its topic changed, archived, live.
135 const [channel, setChannel] = useState<Channel>(data.channel);
136 const [canManage, setCanManage] = useState(data.can_manage === true);
137 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)138 // The server's answer behind a cached draw: merged in by id.
139 useEffect(() => {
140 const onFresh = (event: Event) => {
141 const { key, value } = (event as CustomEvent<{ key: string; value: ChannelData }>).detail;
142 if (key !== window.location.pathname || value.unavailable || value.channel.id !== data.channel.id) return;
143 setMessages((now) => mergeMessages(now, value.messages));
144 setMembers(value.members);
145 setJoined(value.joined);
Chat controls, public profiles, shadcn selects, and no Docs tab in a project146 setChannel(value.channel);
147 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)148 };
149 window.addEventListener("g1t:chat-fresh", onFresh);
150 return () => window.removeEventListener("g1t:chat-fresh", onFresh);
151 }, [data.channel.id]);
152 // The cache follows what is on screen, so coming back shows the latest.
153 useEffect(() => {
154 const path = window.location.pathname;
155 const timer = setTimeout(
Chat controls, public profiles, shadcn selects, and no Docs tab in a project156 () => 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)157 400,
158 );
159 return () => clearTimeout(timer);
Chat controls, public profiles, shadcn selects, and no Docs tab in a project160 }, [view, messages, members, joined, canManage]);
161 // Renamed while open, here or by someone else: the address follows the name.
162 const navigate = useNavigate();
163 const nameShown = useRef(data.channel.name);
164 useEffect(() => {
165 if (channel.kind !== "channel" || channel.name === nameShown.current || !slug) return;
166 nameShown.current = channel.name;
167 navigate(`${channelPath(slug, channel)}${window.location.search}`, { replace: true, preventScrollReset: true });
168 }, [channel, slug, navigate]);
Chat and workspace agents: channels, DMs and named agents you talk to169 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)170 // A phone: back to the list, by the chevron or a swipe from the edge;
171 // and a long-pressed message's actions.
172 const back = useBack(`/${chatData?.slug ?? ""}/-/chat`);
173 const swipe = useSwipeBack(back);
174 const [actions, setActions] = useState<ShownMessage | null>(null);
175 // Someone's profile, opened from their card, beside the conversation.
176 const [profile, setProfile] = useState<string | null>(null);
Merge main into Artifacts Phase 2177 // A message being edited: in its place on a computer (`inline`), in a sheet on a phone.
178 const [editing, setEditing] = useState<{ id: string; body: string; inline?: boolean } | null>(null);
Chat and workspace agents: channels, DMs and named agents you talk to179 const scroller = useRef<HTMLDivElement>(null);
180 const stick = useRef(true);
181
182 const entry = sidebar?.entries.find((e) => e.channel.id === data.channel.id);
183 const starred = entry?.starred ?? members.find((m) => m.member.kind === "user" && m.member.id === me?.id)?.starred ?? false;
184 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.185
186 // Sounds know which conversation is on screen: it is silent while looked at (lib/sound-events.ts).
187 useEffect(() => {
188 viewingConversation(data.channel.id);
189 return () => viewingConversation(null);
190 }, [data.channel.id]);
Chat and workspace agents: channels, DMs and named agents you talk to191 const others = members.filter((m) => !(m.member.kind === "user" && m.member.id === me?.id)).map((m) => m.member);
192 const isDm = data.channel.kind === "dm";
193
194 // The details panel: open by default on a wide screen, as last left.
195 useEffect(() => {
196 try {
197 const saved = localStorage.getItem("chat-info");
198 setInfo(saved ? saved === "1" : window.matchMedia("(min-width: 1280px)").matches && !isDm);
199 } catch {
200 setInfo(false);
201 }
202 }, [isDm]);
203 const toggleInfo = () =>
204 setInfo((now) => {
205 try {
206 localStorage.setItem("chat-info", now ? "0" : "1");
207 } catch {
208 // Not kept: fine.
209 }
210 return !now;
211 });
212
213 const people = useMemo<Mentionable[]>(
214 () => [
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.215 // A personal agent is never mentioned: it answers only in its own DM.
216 ...(chatData?.agents ?? [])
217 .filter((agent) => agent.scope !== "personal")
218 .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 to219 ...(chatData?.people ?? []).filter((p) => p.name !== me?.username),
220 ],
221 [chatData, me?.username],
222 );
223 const context = useMemo<TextContext>(
224 () => ({
225 slug,
226 me: me?.username ?? null,
227 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 calendar228 // `@ana` reads as the name people know: everyone in the workspace, the viewer too.
229 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 to230 channels: new Set((sidebar?.entries ?? []).filter((e) => e.channel.kind === "channel").map((e) => e.channel.name ?? "")),
231 project,
232 codeLink: code ? undefined : (href: string) => (/^\/[^/]+\/(?!-\/)[^/]+/.test(href) ? codeAccessPath(slug, href) : href),
233 }),
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar234 [slug, me, people, chatData?.agents, sidebar?.entries, project, code],
Chat and workspace agents: channels, DMs and named agents you talk to235 );
236
237 // Keeps the newest message in view while the reader is at the bottom.
238 const toBottom = useCallback((smooth = false) => {
239 const element = scroller.current;
240 if (element) element.scrollTo({ top: element.scrollHeight, behavior: smooth ? "smooth" : "auto" });
241 }, []);
242 useEffect(() => toBottom(), [toBottom]);
243 useEffect(() => {
244 if (stick.current) toBottom(true);
245 }, [messages.length, typing.size, toBottom]);
246
247 // Read up to the newest message whenever it is in view.
248 const lastRead = useRef<string | null>(null);
249 const markRead = useCallback(() => {
250 if (!joined || document.visibilityState !== "visible") return;
251 const newest = [...messages].reverse().find((m) => !m.pending);
252 if (!newest || newest.id === lastRead.current) return;
253 lastRead.current = newest.id;
254 void send({ intent: "read", channel_id: data.channel.id, id: newest.id });
255 window.dispatchEvent(new CustomEvent("g1t:chat-read", { detail: data.channel.id }));
256 }, [messages, joined, send, data.channel.id]);
257 useEffect(() => {
258 const timer = setTimeout(markRead, 400);
259 document.addEventListener("visibilitychange", markRead);
260 window.addEventListener("focus", markRead);
261 return () => {
262 clearTimeout(timer);
263 document.removeEventListener("visibilitychange", markRead);
264 window.removeEventListener("focus", markRead);
265 };
266 }, [markRead]);
267
268 // The thread open beside the conversation, from `?thread=`.
269 const loadThread = useCallback(async () => {
270 if (!threadId) return setThread(null);
271 try {
272 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 thread273 const result = (await response.json()) as Result<{ messages: ChatMessage[]; root?: ChatMessage | null }>;
Chat and workspace agents: channels, DMs and named agents you talk to274 // The page reaches back to the message the thread is under: shown above it, not among the replies.
275 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 thread276 const root = result.ok ? (result.value.root ?? result.value.messages.find((m) => m.id === threadId) ?? null) : null;
277 setThreadRoot(root);
Chat and workspace agents: channels, DMs and named agents you talk to278 } catch {
279 setThread([]);
280 }
281 }, [threadId, slug, data.channel.id]);
282 useEffect(() => {
283 setThread(null);
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread284 setThreadRoot(null);
Chat and workspace agents: channels, DMs and named agents you talk to285 void loadThread();
286 }, [loadThread]);
287
288 // Reply counts: counted here only until the service's own count for that
289 // message arrives (a `message.updated` of it), which can come before or
290 // after the reply itself; never twice for one reply.
291 const serverCounted = useRef(new Set<string>());
292 const countedReplies = useRef(new Set<string>());
293 const countReply = useCallback((root: string, reply: string, at: string) => {
294 if (serverCounted.current.has(root) || countedReplies.current.has(reply)) return;
295 countedReplies.current.add(reply);
296 setMessages((now) => now.map((m) => (m.id === root ? { ...m, reply_count: m.reply_count + 1, last_reply_at: at } : m)));
297 }, []);
298 const latest = useRef(messages);
299 latest.current = messages;
300
301 // Everything that happens while the page is open.
302 const onEvent = useCallback(
303 (event: ChatLiveEvent) => {
304 if (event.type === "message.created" || event.type === "message.updated") {
305 const message = event.message;
306 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.307 // A sound, when the rules say so: not for your own, not while looking at it in front.
308 if (event.type === "message.created") chatMessageArrived(message, data.channel.kind, { dnd: dndNow(), muted });
Chat and workspace agents: channels, DMs and named agents you talk to309 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)310 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 to311 if (event.type === "message.created") countReply(message.thread_root, message.id, message.created_at);
312 } else {
313 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)314 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 thread315 // The open thread's root, a session's card say, changes in place at its top too.
316 if (message.id === threadId) setThreadRoot((now) => (now ? keepMine([now], message) : now));
Chat and workspace agents: channels, DMs and named agents you talk to317 }
318 // Whoever just spoke is no longer typing.
319 setTyping((now) => {
320 const key = `${message.author.kind}:${message.author.id}`;
321 if (!now.has(key)) return now;
322 const next = new Map(now);
323 next.delete(key);
324 return next;
325 });
326 } else if (event.type === "message.deleted") {
327 if (event.channel_id !== data.channel.id) return;
328 const gone = (m: ShownMessage) => (m.id === event.id ? { ...m, deleted_at: new Date().toISOString() } : m);
329 setMessages((now) => now.map(gone));
330 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 thread331 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)332 } else if (event.type === "reaction.added" || event.type === "reaction.removed") {
333 if (event.channel_id !== data.channel.id) return;
334 setMessages((now) => applyReactionEvent(now, event, me?.id ?? null));
335 setThread((now) => (now ? applyReactionEvent(now, event, me?.id ?? null) : now));
Chat and workspace agents: channels, DMs and named agents you talk to336 } else if (event.type === "typing") {
337 if (event.channel_id !== data.channel.id) return;
338 if (event.member.kind === "user" && event.member.id === me?.id) return;
339 setTyping((now) => new Map(now).set(`${event.member.kind}:${event.member.id}`, { member: event.member, until: new Date(event.until).getTime() }));
Chat controls, public profiles, shadcn selects, and no Docs tab in a project340 } else if (event.type === "channel.updated") {
341 // Renamed, a new topic, archived or back: everyone looking sees it at once.
342 if (event.channel.id !== data.channel.id) return;
343 setChannel(event.channel);
344 refresh();
Chat and workspace agents: channels, DMs and named agents you talk to345 }
346 },
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.347 [data.channel.id, data.channel.kind, muted, threadId, me?.id, countReply, refresh],
Chat and workspace agents: channels, DMs and named agents you talk to348 );
349 // Back after a drop: what was missed.
350 const onReconnect = useCallback(async () => {
351 const base = `/${slug}/-/chat/api?channel=${encodeURIComponent(data.channel.id)}`;
352 try {
353 // Everything after the newest message held, page by page, oldest first;
354 // what was deleted meanwhile comes back empty and is dropped.
355 let after = [...latest.current].reverse().find((m) => !m.pending)?.id ?? null;
356 for (let pages = 0; after && pages < 20; pages++) {
357 const response = await fetch(`${base}&after=${encodeURIComponent(after)}`);
358 const result = (await response.json()) as Result<{ messages: ChatMessage[]; newer?: string | null }>;
359 if (!result.ok) break;
360 const gone = new Set(result.value.messages.filter((m) => m.deleted_at).map((m) => m.id));
361 const kept = result.value.messages.filter((m) => !m.deleted_at && !m.thread_root);
362 setMessages((now) => mergeMessages(now.filter((m) => !gone.has(m.id)), kept));
363 after = result.value.newer ?? null;
364 }
365 // Edits and deletions to what was already on screen: the latest page again.
366 const response = await fetch(base);
367 const result = (await response.json()) as Result<{ messages: ChatMessage[] }>;
368 if (result.ok) {
369 const gone = new Set(result.value.messages.filter((m) => m.deleted_at).map((m) => m.id));
370 setMessages((now) => mergeMessages(now.filter((m) => !gone.has(m.id)), result.value.messages.filter((m) => !m.deleted_at)));
371 }
372 } catch {
373 // The next event, or the next return, catches up.
374 }
375 void loadThread();
376 refresh();
377 }, [slug, data.channel.id, loadThread, refresh]);
378 const live = useChatLive(slug, data.channel.id, onEvent, onReconnect);
379
380 // Typing shows until its time runs out.
381 useEffect(() => {
382 if (typing.size === 0) return;
383 const timer = setInterval(() => {
384 const now = Date.now();
385 setTyping((current) => {
386 const next = new Map([...current].filter(([, value]) => value.until > now));
387 return next.size === current.size ? current : next;
388 });
389 }, 1000);
390 return () => clearInterval(timer);
391 }, [typing.size]);
392
393 // Says "is typing" to the others, at most every few seconds.
394 const lastTyping = useRef(0);
395 const onTyping = useCallback(() => {
396 const now = Date.now();
397 if (now - lastTyping.current < 3000) return;
398 lastTyping.current = now;
399 live.send({ type: "typing", channel_id: data.channel.id });
400 }, [live, data.channel.id]);
401
402 const post = useCallback(
403 async (body: string, root: string | null) => {
404 if (!me) return;
405 const clientId = newClientId();
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar406 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 to407 const pending: ShownMessage = {
408 id: clientId,
409 client_id: clientId,
410 pending: true,
411 channel_id: data.channel.id,
412 author,
413 kind: "text",
414 body,
415 card: null,
416 thread_root: root,
417 reply_count: 0,
418 last_reply_at: null,
419 created_at: new Date().toISOString(),
420 edited_at: null,
421 deleted_at: null,
422 };
423 const put = (list: ShownMessage[]) => mergeMessages(list, [pending]);
424 if (root) setThread((now) => put(now ?? []));
425 else {
426 stick.current = true;
427 setMessages(put);
428 }
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.429 messageSent({ dnd: dndNow() });
Chat and workspace agents: channels, DMs and named agents you talk to430 const saved = await send<ChatMessage>({ intent: "post", channel_id: data.channel.id, body, thread_root: root });
431 const settle = (list: ShownMessage[]) =>
432 saved.ok
433 ? mergeMessages(list, [{ ...saved.value, client_id: clientId }])
434 : list.map((m) => (m.client_id === clientId ? { ...m, failed: true, pending: true } : m));
435 if (root) {
436 setThread((now) => (now ? settle(now) : now));
437 if (saved.ok) countReply(root, saved.value.id, saved.value.created_at);
438 } else setMessages(settle);
439 },
440 [me, send, data.channel.id, countReply],
441 );
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)442 // ── Reactions (components/emoji): shown at once, then as the service counts them.
443 const customs = useCustomEmoji(slug);
444 const { usercontent } = useAddresses();
445 const meProfile = useMemo<MemberProfile | null>(
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar446 () => (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)447 [me],
448 );
449 const react = useCallback(
450 async (messageId: string, emoji: string, on: boolean) => {
451 if (!meProfile) return;
452 const change = (to: boolean) => (list: ShownMessage[]) =>
453 list.map((m) => (m.id === messageId ? { ...m, reactions: toggleReaction(m.reactions ?? [], emoji, to, meProfile) } : m));
454 setMessages(change(on));
455 setThread((now) => (now ? change(on)(now) : now));
456 try {
457 const response = await fetch(`/${slug}/-/chat/api`, {
458 method: "POST",
459 headers: { "content-type": "application/json" },
460 body: JSON.stringify({ intent: on ? "react" : "unreact", channel_id: data.channel.id, message_id: messageId, emoji }),
461 });
462 const result = (await response.json()) as Result<ChatMessage["reactions"]>;
463 if (!result.ok) throw new Error(result.error.message);
464 const settle = (list: ShownMessage[]) => list.map((m) => (m.id === messageId ? { ...m, reactions: result.value } : m));
465 setMessages(settle);
466 setThread((now) => (now ? settle(now) : now));
467 } catch {
468 // Not saved: back as it was.
469 setMessages(change(!on));
470 setThread((now) => (now ? change(!on)(now) : now));
471 }
472 },
473 [meProfile, slug, data.channel.id],
474 );
475 const emojiContext = useMemo(
476 () => ({ customs, usercontent, me: meProfile, react: (id: string, emoji: string, on: boolean) => void react(id, emoji, on), manageHref: `/${slug}/-/emoji` }),
477 [customs, usercontent, meProfile, react, slug],
478 );
479
Chat and workspace agents: channels, DMs and named agents you talk to480 const retry = (message: ShownMessage) => {
481 const drop = (list: ShownMessage[]) => list.filter((m) => m.client_id !== message.client_id);
482 if (message.thread_root) setThread((now) => (now ? drop(now) : now));
483 else setMessages(drop);
484 void post(message.body, message.thread_root);
485 };
486
487 const loadOlder = async () => {
488 if (!older || loadingOlder) return;
489 setLoadingOlder(true);
490 const element = scroller.current;
491 const fromBottom = element ? element.scrollHeight - element.scrollTop : 0;
492 try {
493 const response = await fetch(`/${slug}/-/chat/api?channel=${encodeURIComponent(data.channel.id)}&before=${encodeURIComponent(older)}`);
494 const result = (await response.json()) as Result<{ messages: ChatMessage[]; older: string | null }>;
495 if (result.ok) {
496 stick.current = false;
497 setMessages((now) => mergeMessages(now, result.value.messages));
498 setOlder(result.value.older);
499 requestAnimationFrame(() => {
500 if (element) element.scrollTop = element.scrollHeight - fromBottom;
501 });
502 }
503 } finally {
504 setLoadingOlder(false);
505 }
506 };
507
508 const openThread = (id: string | null) => {
509 const next = new URLSearchParams(params);
510 if (id) next.set("thread", id);
511 else next.delete("thread");
512 setParams(next, { preventScrollReset: true, replace: !!threadId && !!id });
513 };
514
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.515 // A thread's link, to copy and to cite; and "Write this up as an artifact",
516 // 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 thread517 const [writeUp, setWriteUp] = useState<string | null>(null);
518 const linkToThread = (root: string) => threadLink(window.location.origin, channelPath(slug, channel), root);
519 const copyThreadLink = (root: string) => {
520 const done = navigator.clipboard?.writeText(linkToThread(root));
521 if (!done) return showToast(false, "Couldn't copy the link here.");
522 done.then(
523 () => showToast(true, "Link to the thread copied."),
524 () => showToast(false, "Couldn't copy the link here."),
525 );
526 };
527 const writers = useMemo(() => writeUpAgents(members.map((m) => m.member)), [members]);
528 const askForWriteUp = async (body: string): Promise<string | null> => {
529 const root = writeUp;
530 if (!root) return null;
531 // The thread open already: posted as any reply is, shown at once.
532 if (root === threadId) {
533 void post(body, root);
534 return null;
535 }
536 const saved = await send<ChatMessage>({ intent: "post", channel_id: data.channel.id, body, thread_root: root });
537 if (!saved.ok) return saved.error.message;
538 openThread(root);
539 return null;
540 };
541
Chat controls, public profiles, shadcn selects, and no Docs tab in a project542 const updateChannel = useCallback(
543 async (change: ChannelChange): Promise<Result<unknown>> => {
544 const done = await send<{ channel: Channel }>({ intent: "update_channel", channel_id: data.channel.id, ...change });
545 if (done.ok) setChannel(done.value.channel);
546 return done;
547 },
548 [send, data.channel.id],
549 );
550
Chat and workspace agents: channels, DMs and named agents you talk to551 const toggleStar = async () => {
552 await send({ intent: "preferences", channel_id: data.channel.id, starred: !starred });
553 refresh();
554 };
555
556 const join = async () => {
557 const done = await send({ intent: "join", channel_id: data.channel.id });
558 if (done.ok) {
559 setJoined(true);
560 if (me) {
561 setMembers((now) => [
562 ...now,
563 {
564 channel_id: data.channel.id,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar565 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 to566 role: "member",
567 starred: false,
568 muted: false,
569 last_read_id: null,
570 joined_at: new Date().toISOString(),
571 },
572 ]);
573 }
574 }
575 };
576
577 const rows = useMemo(() => timeline(messages, new Date(), zone), [messages, zone]);
Merge main into Artifacts Phase 2578 const mine = (message: ShownMessage) => message.author.kind === "user" && message.author.id === me?.id && !message.pending && !message.deleted_at;
579 // Editing uses the composer, started from the message's Markdown.
580 const editor = (target: { id: string; body: string }) => (
581 <Composer
582 initial={target.body}
583 placeholder="Edit message"
584 people={people}
585 autoFocus
586 compact
587 onCancel={() => setEditing(null)}
588 onSend={async (body) => {
589 setEditing(null);
590 if (body === target.body) return;
591 // Shown edited at once; put back if the service refuses.
592 const before = messages.find((m) => m.id === target.id);
593 setMessages((now) => now.map((m) => (m.id === target.id ? { ...m, body, edited_at: new Date().toISOString() } : m)));
594 const saved = await send<ChatMessage>({ intent: "edit", channel_id: data.channel.id, id: target.id, body });
595 if (saved.ok) setMessages((now) => mergeMessages(now, [saved.value]));
596 else if (before) setMessages((now) => now.map((m) => (m.id === before.id ? before : m)));
597 }}
598 />
599 );
Chat and workspace agents: channels, DMs and named agents you talk to600 const typers = [...typing.values()].map((t) => t.member);
Chat controls, public profiles, shadcn selects, and no Docs tab in a project601 const name = channel.name ?? "";
602 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.603 // A mixed direct message names each member and says which are agents.
Chat and workspace agents: channels, DMs and named agents you talk to604 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.605 ? isMixed(others)
606 ? dmPlaceholder(others)
607 : `Message ${others.map(shownName).join(", ") || "yourself"}`
Chat and workspace agents: channels, DMs and named agents you talk to608 : `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 thread609 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)610 const cardContext = useMemo(
611 () => ({
612 slug,
613 agents: chatData?.agents ?? [],
614 onViewProfile: (username: string) => {
615 if (threadId) openThread(null);
616 setProfile(username);
617 },
618 }),
619 // eslint-disable-next-line react-hooks/exhaustive-deps
620 [slug, chatData?.agents, threadId],
621 );
Chat and workspace agents: channels, DMs and named agents you talk to622
623 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)624 // On a phone the conversation is the whole screen, as tall as what the
625 // keyboard leaves (components/mobile.tsx), so the composer sits on it.
626 <EmojiProvider value={emojiContext}>
627 <CardContext.Provider value={cardContext}>
628 <div
629 {...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.630 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)631 >
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar632 {/* What pressing a card's action did (components/chat/card.tsx). */}
633 <CardToasts />
Chat and workspace agents: channels, DMs and named agents you talk to634 <section aria-label={data.title} className="flex min-w-0 grow flex-col">
635 <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)636 onBack={back}
Chat controls, public profiles, shadcn selects, and no Docs tab in a project637 data={view}
Chat and workspace agents: channels, DMs and named agents you talk to638 others={others}
639 starred={starred}
640 muted={muted}
641 memberCount={members.length}
642 live={live.state}
643 info={info}
644 onStar={() => void toggleStar()}
645 onInfo={toggleInfo}
646 />
647 <div
648 ref={scroller}
649 onScroll={(event) => {
650 const element = event.currentTarget;
651 stick.current = element.scrollHeight - element.scrollTop - element.clientHeight < 120;
652 }}
653 className="min-h-0 grow overflow-y-auto overscroll-contain [scrollbar-gutter:stable] [scrollbar-width:thin]"
654 >
655 <div className="mx-auto flex min-h-full max-w-[56rem] flex-col justify-end px-2 pt-6 pb-3 sm:px-4">
656 {older ? (
657 <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.658 <Button
Chat and workspace agents: channels, DMs and named agents you talk to659 type="button"
660 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.661 variant="outline"
662 size="inline"
663 className="rounded-full px-3 py-1 text-xs text-muted font-normal"
Chat and workspace agents: channels, DMs and named agents you talk to664 >
665 {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.666 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to667 </div>
668 ) : (
Chat controls, public profiles, shadcn selects, and no Docs tab in a project669 <ConversationStart data={view} others={others} />
Chat and workspace agents: channels, DMs and named agents you talk to670 )}
671 {rows.map((row) =>
672 row.kind === "day" ? (
673 <DayRule key={row.key} label={row.label} />
674 ) : (
675 <MessageRow
676 key={row.key}
677 message={row.message}
678 head={row.head}
679 zone={zone}
680 context={context}
681 code={code}
682 slug={slug}
683 onThread={() => openThread(row.message.id)}
684 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)685 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 thread686 onCopyLink={() => copyThreadLink(row.message.id)}
Merge main into Artifacts Phase 2687 onEdit={mine(row.message) ? () => setEditing({ id: row.message.id, body: row.message.body, inline: true }) : undefined}
688 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 thread689 onWriteUp={joined && !archived ? () => setWriteUp(row.message.id) : undefined}
Chat and workspace agents: channels, DMs and named agents you talk to690 threadOpen={row.message.id === threadId}
691 />
692 ),
693 )}
694 </div>
695 </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)696 <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 to697 <div className="mx-auto max-w-[56rem]">
698 <TypingLine members={typers} />
Chat controls, public profiles, shadcn selects, and no Docs tab in a project699 {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.700 <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 project701 <p className="flex items-center gap-2 text-sm text-muted">
702 <Archive size={15} className="shrink-0 text-faint" />
703 <span>
704 <span className="font-medium text-fg">#{name}</span> is archived. Its history stays here to read; nobody can post in it.
705 </span>
706 </p>
707 {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.708 <Button
Chat controls, public profiles, shadcn selects, and no Docs tab in a project709 type="button"
710 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.711 variant="outline"
712 className="px-3 text-fg/90 hover:bg-raised"
Chat controls, public profiles, shadcn selects, and no Docs tab in a project713 >
714 <ArchiveRestore size={15} />
715 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.716 </Button>
Chat controls, public profiles, shadcn selects, and no Docs tab in a project717 )}
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.718 </Card>
Chat controls, public profiles, shadcn selects, and no Docs tab in a project719 ) : joined ? (
Chat and workspace agents: channels, DMs and named agents you talk to720 <Composer draftKey={data.channel.id} placeholder={placeholder} people={people} onSend={(body) => void post(body, null)} onTyping={onTyping} autoFocus />
721 ) : (
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.722 <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 to723 <p className="text-sm text-muted">
724 You're reading <span className="font-medium text-fg">#{name}</span>. Join to post and get its notifications.
725 </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.726 <Button
Chat and workspace agents: channels, DMs and named agents you talk to727 type="button"
728 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.729 variant="accent"
Chat and workspace agents: channels, DMs and named agents you talk to730 >
731 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.732 </Button>
733 </Card>
Chat and workspace agents: channels, DMs and named agents you talk to734 )}
735 </div>
736 </div>
737 </section>
738 {threadId ? (
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread739 <SidePanel
740 title="Thread"
741 subtitle={isDm ? undefined : `#${name}`}
742 onClose={() => openThread(null)}
743 menu={<ThreadMenu onCopyLink={() => copyThreadLink(threadId)} onWriteUp={joined && !archived ? () => setWriteUp(threadId) : undefined} />}
744 >
Chat and workspace agents: channels, DMs and named agents you talk to745 <ThreadPanel
746 root={rootMessage}
747 replies={thread}
748 zone={zone}
749 context={context}
750 code={code}
751 slug={slug}
752 people={people}
Chat controls, public profiles, shadcn selects, and no Docs tab in a project753 joined={joined && !archived}
Chat and workspace agents: channels, DMs and named agents you talk to754 onSend={(body) => void post(body, threadId)}
755 onRetry={retry}
Chat controls, public profiles, shadcn selects, and no Docs tab in a project756 onTyping={onTyping}
Chat and workspace agents: channels, DMs and named agents you talk to757 draftKey={`${data.channel.id}:${threadId}`}
758 />
759 </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)760 ) : profile ? (
761 <SidePanel title="Profile" onClose={() => setProfile(null)}>
762 <ProfilePanel slug={slug} username={profile} messages={messages} zone={zone} context={context} />
763 </SidePanel>
Chat and workspace agents: channels, DMs and named agents you talk to764 ) : (
765 info && (
766 <SidePanel title={isDm ? "Details" : "About this channel"} onClose={toggleInfo}>
Chat controls, public profiles, shadcn selects, and no Docs tab in a project767 <InfoPanel
768 data={view}
769 members={members}
770 slug={slug}
771 people={people}
772 onAdded={(m) => setMembers((now) => [...now, m])}
773 joined={joined}
774 canManage={canManage}
775 onUpdate={updateChannel}
776 />
Chat and workspace agents: channels, DMs and named agents you talk to777 </SidePanel>
778 )
779 )}
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)780 <MessageActions
781 message={actions}
782 mine={actions != null && actions.author.kind === "user" && actions.author.id === me?.id}
783 onClose={() => setActions(null)}
784 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 thread785 onCopyLink={(message) => copyThreadLink(message.thread_root ?? message.id)}
786 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)787 onEdit={(message) => setEditing({ id: message.id, body: message.body })}
788 onDelete={async (message) => {
789 // Gone at once; back if the service refuses.
790 setMessages((now) => now.map((m) => (m.id === message.id ? { ...m, deleted_at: new Date().toISOString() } : m)));
791 const done = await send({ intent: "remove", channel_id: data.channel.id, id: message.id });
792 if (!done.ok) setMessages((now) => now.map((m) => (m.id === message.id ? { ...m, deleted_at: null } : m)));
793 }}
794 />
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread795 <WriteUpDialog
796 open={writeUp != null}
797 onOpenChange={(open) => !open && setWriteUp(null)}
798 slug={slug}
799 agents={writers}
800 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.801 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 thread802 onSend={askForWriteUp}
803 />
Merge main into Artifacts Phase 2804 <BottomSheet open={editing != null && !editing.inline} onOpenChange={(open) => !open && setEditing(null)} title="Edit message">
805 {editing && !editing.inline && (
806 <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)807 <h2 className="px-2 text-base font-semibold">Edit message</h2>
Merge main into Artifacts Phase 2808 {editor(editing)}
809 </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)810 )}
811 </BottomSheet>
Chat and workspace agents: channels, DMs and named agents you talk to812 </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)813 </CardContext.Provider>
814 </EmojiProvider>
Chat and workspace agents: channels, DMs and named agents you talk to815 );
816}
817
818/** The top of a conversation: what it is, and its controls. */
819function 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)820 onBack,
Chat and workspace agents: channels, DMs and named agents you talk to821 data,
822 others,
823 starred,
824 muted,
825 memberCount,
826 live,
827 info,
828 onStar,
829 onInfo,
830}: {
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 /** A phone: back to the list of conversations. */
832 onBack: () => void;
Chat and workspace agents: channels, DMs and named agents you talk to833 data: Loaded;
834 others: MemberProfile[];
835 starred: boolean;
836 muted: boolean;
837 memberCount: number;
838 live: LiveState;
839 info: boolean;
840 onStar: () => void;
841 onInfo: () => void;
842}) {
843 const channel = data.channel;
844 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.845 const mixed = channel.kind === "dm" && isMixed(others);
Chat and workspace agents: channels, DMs and named agents you talk to846 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.847 <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">
848 <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)849 <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.850 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to851 <div className="flex min-w-0 grow items-center gap-2.5">
852 {channel.kind === "channel" ? (
853 <>
854 <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 project855 {channel.private ? (
856 <Hint label="Private channel: only its members can find and read it">
857 <span className="flex">
858 <Lock size={15} className="text-faint" aria-label="Private channel" />
859 </span>
860 </Hint>
861 ) : (
862 <Hash size={16} className="text-faint" aria-label="Public channel" />
863 )}
Chat and workspace agents: channels, DMs and named agents you talk to864 <span className="truncate">{channel.name}</span>
865 </h1>
866 <StarButton starred={starred} onClick={onStar} />
867 {muted && (
868 <Hint label="Muted: no unread count or notifications">
869 <span className="text-faint">
870 <BellOff size={14} aria-label="Muted" />
871 </span>
872 </Hint>
873 )}
Chat controls, public profiles, shadcn selects, and no Docs tab in a project874 {channel.archived_at && (
875 <Badge tone="neutral" className="shrink-0">
876 <Archive size={11} />
877 Archived
878 </Badge>
879 )}
Chat and workspace agents: channels, DMs and named agents you talk to880 {channel.topic && (
881 <>
882 <span aria-hidden="true" className="hidden h-4 w-px shrink-0 bg-line sm:block" />
883 <p className="hidden min-w-0 truncate text-sm text-muted sm:block">{channel.topic}</p>
884 </>
885 )}
886 </>
887 ) : (
888 <>
889 <span className="flex shrink-0 -space-x-1.5">
890 {others.slice(0, 3).map((member) => (
891 <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 managers892 <MemberAvatar member={member} size={26} presence={others.length === 1} />
Chat and workspace agents: channels, DMs and named agents you talk to893 </span>
894 ))}
895 </span>
896 <div className="min-w-0">
897 <h1 className="flex min-w-0 items-center gap-1.5 text-[0.9375rem] leading-tight font-semibold">
898 <span className="truncate">{others.map(shownName).join(", ") || "Just you"}</span>
899 {agentDm && <AgentPill />}
900 </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)901 {agentDm && (titleOf(agentDm) ?? agentDm.role) && (
902 <p className="truncate text-xs leading-tight text-muted">{titleOf(agentDm) ?? agentDm.role}</p>
903 )}
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.904 {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 managers905 {/* One person: their status under their name, as their card has it. */}
906 {!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 to907 </div>
908 <StarButton starred={starred} onClick={onStar} />
909 </>
910 )}
911 </div>
912 {live !== "open" && (
913 <span role="status" className="hidden items-center gap-1.5 text-xs text-faint sm:flex">
914 <span className="size-1.5 animate-pulse rounded-full bg-warn" />
915 {live === "reconnecting" ? "Reconnecting…" : "Connecting…"}
916 </span>
917 )}
918 {channel.kind === "channel" && (
919 <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.920 <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 to921 <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.922 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to923 </Hint>
924 )}
925 {channel.kind === "dm" && (
926 <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.927 <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 to928 <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.929 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to930 </Hint>
931 )}
932 </header>
933 );
934}
935
936function UsersStack({ count }: { count: number }) {
937 return (
938 <>
939 <Users size={14} />
940 {count}
941 </>
942 );
943}
944
945function StarButton({ starred, onClick }: { starred: boolean; onClick: () => void }) {
946 return (
947 <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.948 <Button
949 variant="ghost"
950 size="icon-sm"
Chat and workspace agents: channels, DMs and named agents you talk to951 aria-label={starred ? "Unstar" : "Star"}
952 aria-pressed={starred}
953 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.954 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 to955 >
956 <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.957 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to958 </Hint>
959 );
960}
961
962/** How a conversation begins, above its first message. */
963function ConversationStart({ data, others }: { data: Loaded; others: MemberProfile[] }) {
964 const channel = data.channel;
965 if (channel.kind === "dm") {
966 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.967 const mixed = isMixed(others);
Chat and workspace agents: channels, DMs and named agents you talk to968 return (
969 <div className="px-2 pb-6 sm:px-3">
970 <div className="flex -space-x-2">
971 {others.slice(0, 4).map((m) => (
972 <span key={m.id} className="rounded-full ring-4 ring-bg">
973 <MemberAvatar member={m} size={56} />
974 </span>
975 ))}
976 </div>
977 <h2 className="mt-3 flex items-center gap-2 text-xl font-semibold tracking-tight">
978 {others.map(shownName).join(", ") || "Just you"}
979 {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.980 {mixed && (
981 <Badge tone="accent" size="md" className="font-normal">
982 with an agent
983 </Badge>
984 )}
Chat and workspace agents: channels, DMs and named agents you talk to985 </h2>
986 <p className="mt-1 max-w-xl text-sm text-muted">
987 {agent
988 ? `${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.989 : mixed
990 ? "A conversation between people and an agent. Everyone here, the agent too, reads every message."
991 : "This is the start of your conversation."}
Chat and workspace agents: channels, DMs and named agents you talk to992 </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.993 {mixed && <MixedNotice channelId={channel.id} others={others} />}
Chat and workspace agents: channels, DMs and named agents you talk to994 </div>
995 );
996 }
997 return (
998 <div className="px-2 pb-6 sm:px-3">
999 <span className="flex size-12 items-center justify-center rounded-xl bg-raised text-muted">
1000 {channel.private ? <Lock size={22} /> : <Hash size={24} />}
1001 </span>
1002 <h2 className="mt-3 text-xl font-semibold tracking-tight">Welcome to #{channel.name}</h2>
1003 <p className="mt-1 max-w-xl text-sm text-muted">{channel.topic ?? "This is the very beginning of the channel."}</p>
1004 </div>
1005 );
1006}
1007
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.1008/**
1009 * The chip a mixed direct message carries in its header, always: each agent
1010 * in it by face and handle, said to be an agent, so a person writing to a
1011 * colleague never forgets who else reads along.
1012 */
1013function MixedChip({ agents }: { agents: MemberProfile[] }) {
1014 const handles = agents.map((a) => `@${shownHandle(a)}`);
1015 const list = handles.length <= 2 ? handles.join(" and ") : `${handles.slice(0, -1).join(", ")} and ${handles[handles.length - 1]}`;
1016 return (
1017 <Badge tone="accent" size="md" className="mt-0.5 max-w-full gap-1.5 pl-1 font-normal">
1018 <span className="flex shrink-0 -space-x-1">
1019 {agents.slice(0, 3).map((a) => (
1020 <AgentAvatar key={a.id} agent={a} size={16} ring="var(--color-bg)" />
1021 ))}
1022 </span>
1023 <span className="truncate">
1024 {list}, {agents.length === 1 ? "agent" : "agents"}
1025 <span className="max-md:hidden">, {agents.length === 1 ? "is" : "are"} in this conversation</span>
1026 </span>
1027 </Badge>
1028 );
1029}
1030
1031const MIXED_SEEN_KEY = (channelId: string) => `g1t:chat-mixed-seen:${channelId}`;
1032
1033/**
1034 * The one-line notice above a mixed conversation's first message, the
1035 * first time a person opens it on this device: who the agents are and
1036 * that they read everything and can act on it. Dismissed, it stays away.
1037 */
1038function MixedNotice({ channelId, others }: { channelId: string; others: MemberProfile[] }) {
1039 const [seen, setSeen] = useState(true);
1040 useEffect(() => {
1041 try {
1042 setSeen(localStorage.getItem(MIXED_SEEN_KEY(channelId)) === "1");
1043 } catch {
1044 setSeen(false);
1045 }
1046 }, [channelId]);
1047 const dismiss = () => {
1048 setSeen(true);
1049 try {
1050 localStorage.setItem(MIXED_SEEN_KEY(channelId), "1");
1051 } catch {
1052 // Not kept: it shows again next time, which is fine.
1053 }
1054 };
1055 const words = mixedNotice(others);
1056 if (seen || !words) return null;
1057 return (
1058 <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">
1059 <span className="flex shrink-0 -space-x-1">
1060 {agentsAmong(others).slice(0, 3).map((a) => (
1061 <AgentAvatar key={a.id} agent={a} size={20} ring="var(--color-bg)" />
1062 ))}
1063 </span>
1064 <span className="min-w-0 grow">{words}</span>
1065 <Hint label="Got it">
1066 <Button type="button" variant="ghost" size="icon-sm" aria-label="Dismiss" onClick={dismiss} className="shrink-0 text-faint">
1067 <X size={14} />
1068 </Button>
1069 </Hint>
1070 </Card>
1071 );
1072}
1073
Chat and workspace agents: channels, DMs and named agents you talk to1074function DayRule({ label }: { label: string }) {
1075 return (
1076 <div role="separator" aria-label={label} className="relative my-3 flex items-center justify-center">
1077 <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.1078 <Badge className="relative bg-bg px-3 py-0.5" suppressHydrationWarning>
Chat and workspace agents: channels, DMs and named agents you talk to1079 {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.1080 </Badge>
Chat and workspace agents: channels, DMs and named agents you talk to1081 </div>
1082 );
1083}
1084
1085/** 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)1086/**
1087 * Someone's profile beside the conversation, opened from their card: who
1088 * they are, their teams and local time, and what they said here lately.
1089 * The conversation stays where it was.
1090 */
1091function ProfilePanel({
1092 slug,
1093 username,
1094 messages,
1095 zone,
1096 context,
1097}: {
1098 slug: string;
1099 username: string;
1100 messages: ShownMessage[];
1101 zone: string | undefined;
1102 context: TextContext;
1103}) {
1104 const [card, setCard] = useState<PersonCard | null | undefined>(undefined);
1105 useEffect(() => {
1106 let live = true;
1107 setCard(undefined);
1108 void personCard(slug, username).then((value) => {
1109 if (live) setCard(value);
1110 });
1111 return () => {
1112 live = false;
1113 };
1114 }, [slug, username]);
1115 const theirs = messages
1116 .filter((m) => m.author.kind === "user" && m.author.name === username && !m.deleted_at && m.body)
1117 .slice(-5)
1118 .reverse();
1119 const time = card ? localTime(card.timezone, Date.now()) : null;
1120 return (
1121 <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 managers1122 <WithPresence person={{ username }} size={88}>
1123 <Avatar name={username} image={card?.avatar ?? null} size={88} />
1124 </WithPresence>
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar1125 <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)1126 <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 calendar1127 @{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)1128 {card?.pronouns ? <span className="font-sans"> · {card.pronouns}</span> : null}
1129 </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 managers1130 <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)1131 {card?.bio && <p className="mt-3 text-sm leading-relaxed text-fg-soft">{card.bio}</p>}
1132 <dl className="mt-4 space-y-2 text-sm">
1133 {card && card.teams.length > 0 && (
1134 <div className="flex gap-3">
1135 <dt className="w-20 shrink-0 text-faint">Teams</dt>
1136 <dd className="text-fg-soft">{card.teams.map((team) => team.name).join(", ")}</dd>
1137 </div>
1138 )}
1139 {time && (
1140 <div className="flex gap-3">
1141 <dt className="w-20 shrink-0 text-faint">Local time</dt>
1142 <dd className="text-fg-soft" suppressHydrationWarning>
1143 {time}
1144 </dd>
1145 </div>
1146 )}
1147 {card?.location && (
1148 <div className="flex gap-3">
1149 <dt className="w-20 shrink-0 text-faint">Location</dt>
1150 <dd className="text-fg-soft">{card.location}</dd>
1151 </div>
1152 )}
1153 {card && card.workspaces.length > 0 && (
1154 <div className="flex gap-3">
1155 <dt className="w-20 shrink-0 text-faint">Workspaces</dt>
1156 <dd className="text-fg-soft">{card.workspaces.map((w) => w.name).join(", ")}</dd>
1157 </div>
1158 )}
1159 </dl>
1160 <h4 className="mt-6 mb-2 text-xs font-medium text-faint">Lately in this conversation</h4>
1161 {theirs.length === 0 ? (
1162 <p className="text-sm text-muted">Nothing here yet.</p>
1163 ) : (
1164 <ul className="space-y-3">
1165 {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.1166 <Card asChild key={m.id} radius="lg" className="px-3 py-2.5">
1167 <li>
1168 <p className="mb-1 text-[0.6875rem] text-faint" suppressHydrationWarning>
1169 {clock(m.created_at, zone)}
1170 </p>
1171 <div className="line-clamp-4 text-sm">
1172 <MessageText body={m.body} context={context} />
1173 </div>
1174 </li>
1175 </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)1176 ))}
1177 </ul>
1178 )}
1179 <Link to={`/u/${username}`} className="mt-6 inline-block text-sm text-accent hover:underline">
1180 Open their full profile
1181 </Link>
1182 </div>
1183 );
1184}
1185
1186/** An agent's title ("QA Engineer"), when the chat service sends it with the member. */
1187function titleOf(member: MemberProfile): string | null {
1188 return (member as MemberProfile & { title?: string | null }).title ?? null;
1189}
1190
1191/** A long press (half a second, without moving) on a touch screen. */
1192function useLongPress(onLongPress?: () => void) {
1193 const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
1194 const cancel = () => {
1195 if (timer.current) clearTimeout(timer.current);
1196 timer.current = null;
1197 };
1198 if (!onLongPress) return {};
1199 return {
1200 onTouchStart: () => {
1201 cancel();
1202 timer.current = setTimeout(() => {
1203 timer.current = null;
1204 if (typeof navigator !== "undefined" && "vibrate" in navigator) navigator.vibrate?.(8);
1205 onLongPress();
1206 }, 480);
1207 },
1208 onTouchMove: cancel,
1209 onTouchEnd: cancel,
1210 onTouchCancel: cancel,
1211 onContextMenu: (event: React.MouseEvent) => {
1212 // A phone's own long-press menu (copy, look up) would cover ours.
1213 if (window.matchMedia("(pointer: coarse)").matches) event.preventDefault();
1214 },
1215 };
1216}
1217
1218/** A long-pressed message's actions, in a sheet from the bottom: reply, copy, and for your own, edit and delete. */
1219function MessageActions({
1220 message,
1221 mine,
1222 onClose,
1223 onThread,
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1224 onCopyLink,
1225 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)1226 onEdit,
1227 onDelete,
1228}: {
1229 message: ShownMessage | null;
1230 mine: boolean;
1231 onClose: () => void;
1232 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 thread1233 onCopyLink: (message: ShownMessage) => void;
1234 /** Absent where the person cannot post (not joined, archived). */
1235 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)1236 onEdit: (message: ShownMessage) => void;
1237 onDelete: (message: ShownMessage) => void;
1238}) {
1239 const [confirm, setConfirm] = useState(false);
1240 useEffect(() => setConfirm(false), [message]);
1241 const act = (run: () => void) => () => {
1242 run();
1243 onClose();
1244 };
1245 return (
1246 <BottomSheet open={message != null} onOpenChange={(open) => !open && onClose()} title="Message actions">
1247 {message && (
1248 <>
1249 <p className="mt-1 mb-2 line-clamp-2 px-3 text-sm text-muted">
1250 <span className="font-medium text-fg-soft">{shownName(message.author)}:</span> {message.body || message.card?.title}
1251 </p>
1252 {!message.pending && <QuickReactions messageId={message.id} onDone={onClose} />}
1253 {!message.thread_root && (
1254 <SheetRow icon={<MessageSquareText />} onClick={act(() => onThread(message.id))}>
1255 Reply in thread
1256 </SheetRow>
1257 )}
1258 <SheetRow icon={<Copy />} onClick={act(() => void navigator.clipboard?.writeText(message.body || message.card?.title || ""))}>
1259 Copy text
1260 </SheetRow>
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1261 {!message.pending && (
1262 <SheetRow icon={<Link2 />} onClick={act(() => onCopyLink(message))}>
1263 Copy link to thread
1264 </SheetRow>
1265 )}
1266 {!message.pending && onWriteUp && (
1267 <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.1268 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 thread1269 </SheetRow>
1270 )}
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)1271 {mine && message.kind === "text" && (
1272 <SheetRow icon={<Pencil />} onClick={act(() => onEdit(message))}>
1273 Edit
1274 </SheetRow>
1275 )}
1276 {mine &&
1277 (confirm ? (
1278 <SheetRow icon={<Trash2 className="text-danger" />} onClick={act(() => onDelete(message))}>
1279 <span className="text-danger">Delete for everyone</span>
1280 </SheetRow>
1281 ) : (
1282 <SheetRow icon={<Trash2 />} onClick={() => setConfirm(true)}>
1283 Delete
1284 </SheetRow>
1285 ))}
1286 </>
1287 )}
1288 </BottomSheet>
1289 );
1290}
1291
Chat and workspace agents: channels, DMs and named agents you talk to1292function MessageRow({
1293 message,
1294 head,
1295 zone,
1296 context,
1297 code,
1298 slug,
1299 onThread,
1300 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)1301 onLongPress,
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1302 onCopyLink,
1303 onWriteUp,
Chat and workspace agents: channels, DMs and named agents you talk to1304 threadOpen,
1305 inThread,
Merge main into Artifacts Phase 21306 onEdit,
1307 editor,
Chat and workspace agents: channels, DMs and named agents you talk to1308}: {
Merge main into Artifacts Phase 21309 /** Your own message: "Edit message" in the "⋯", and the composer in its place while editing. */
1310 onEdit?: () => void;
1311 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)1312 /** A phone: a long press opens the message's actions. */
1313 onLongPress?: () => void;
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1314 /** The hover toolbar's "⋯": copy the link to its thread, write it up in Docs. */
1315 onCopyLink?: () => void;
1316 onWriteUp?: () => void;
Chat and workspace agents: channels, DMs and named agents you talk to1317 message: ShownMessage;
1318 head: boolean;
1319 zone: string | undefined;
1320 context: TextContext;
1321 code: boolean;
1322 slug: string;
1323 onThread?: () => void;
1324 onRetry: () => void;
1325 threadOpen?: boolean;
1326 inThread?: boolean;
1327}) {
1328 const author = message.author;
1329 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)1330 const press = useLongPress(onLongPress);
Chat and workspace agents: channels, DMs and named agents you talk to1331 return (
1332 <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)1333 {...press}
Chat and workspace agents: channels, DMs and named agents you talk to1334 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)1335 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 to1336 threadOpen ? "bg-accent/[0.06] hover:bg-accent/[0.08]" : ""
1337 }`}
1338 >
1339 <div className="w-9 shrink-0">
1340 {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)1341 <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 to1342 <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)1343 </MemberCard>
Chat and workspace agents: channels, DMs and named agents you talk to1344 ) : (
1345 <time
1346 dateTime={message.created_at}
1347 suppressHydrationWarning
1348 className="mt-1 block text-right text-[0.625rem] leading-5 text-faint tabular-nums opacity-0 group-hover/message:opacity-100"
1349 >
1350 {time.replace(/\s?[AP]M$/, "")}
1351 </time>
1352 )}
1353 </div>
1354 <div className="min-w-0 grow">
1355 {head && (
1356 <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)1357 <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 to1358 {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)1359 </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 managers1360 {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 to1361 {author.kind === "agent" && <AgentPill className="self-center" />}
1362 <time dateTime={message.created_at} suppressHydrationWarning className="shrink-0 text-xs text-faint tabular-nums">
1363 {time}
1364 </time>
1365 </div>
1366 )}
1367 <div className={message.pending && !message.failed ? "opacity-60" : undefined}>
Merge main into Artifacts Phase 21368 {editor ? <div className="py-1">{editor}</div> : message.body && <MessageText body={message.body} context={context} />}
1369 {!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 calendar1370 {message.card && (
1371 <CardBox
1372 card={message.card}
1373 slug={slug}
1374 code={code}
1375 channelId={message.channel_id}
1376 messageId={message.id}
1377 inert={!!message.pending || !!message.deleted_at}
1378 />
1379 )}
Chat and workspace agents: channels, DMs and named agents you talk to1380 {message.edited_at && <span className="text-[0.6875rem] text-faint"> (edited)</span>}
1381 </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)1382 {!message.pending && !message.deleted_at && <ReactionBar messageId={message.id} reactions={message.reactions} />}
Chat and workspace agents: channels, DMs and named agents you talk to1383 {message.failed && (
1384 <p className="mt-0.5 text-xs text-danger">
1385 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.1386 <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 to1387 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.1388 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to1389 </p>
1390 )}
1391 {!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.1392 <Button
Chat and workspace agents: channels, DMs and named agents you talk to1393 type="button"
1394 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.1395 variant="ghost"
1396 size="inline"
1397 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 to1398 >
1399 <MessageSquareText size={14} />
1400 {message.reply_count} {message.reply_count === 1 ? "reply" : "replies"}
1401 {message.last_reply_at && (
1402 <span className="font-normal text-faint" suppressHydrationWarning>
1403 Last reply {clock(message.last_reply_at, zone)}
1404 </span>
1405 )}
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.1406 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to1407 )}
1408 </div>
1409 {!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.1410 <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)1411 <AddReaction messageId={message.id} />
Chat and workspace agents: channels, DMs and named agents you talk to1412 <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.1413 <Button
Chat and workspace agents: channels, DMs and named agents you talk to1414 type="button"
1415 aria-label="Reply in thread"
1416 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.1417 variant="ghost"
1418 size="icon-xs"
Chat and workspace agents: channels, DMs and named agents you talk to1419 >
1420 <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.1421 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to1422 </Hint>
Merge main into Artifacts Phase 21423 {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.1424 </Card>
Chat and workspace agents: channels, DMs and named agents you talk to1425 )}
1426 </article>
1427 );
1428}
1429
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1430/** A message's "⋯" in its hover toolbar. */
Merge main into Artifacts Phase 21431function 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 thread1432 return (
1433 <DropdownMenu>
1434 <Hint label="More actions">
1435 <DropdownMenuTrigger
1436 aria-label="More actions"
1437 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"
1438 >
1439 <Ellipsis size={15} />
1440 </DropdownMenuTrigger>
1441 </Hint>
1442 <DropdownMenuContent align="end">
Merge main into Artifacts Phase 21443 {onEdit && (
1444 <DropdownMenuItem onSelect={onEdit}>
1445 <Pencil />
1446 Edit message
1447 </DropdownMenuItem>
1448 )}
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1449 <ThreadMenuItems onCopyLink={onCopyLink} onWriteUp={onWriteUp} />
1450 </DropdownMenuContent>
1451 </DropdownMenu>
1452 );
1453}
1454
1455function ThreadMenuItems({ onCopyLink, onWriteUp }: { onCopyLink: () => void; onWriteUp?: () => void }) {
1456 return (
1457 <>
1458 <DropdownMenuItem onSelect={onCopyLink}>
1459 <Link2 />
1460 Copy link to thread
1461 </DropdownMenuItem>
1462 {onWriteUp && (
1463 <DropdownMenuItem onSelect={onWriteUp}>
1464 <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.1465 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 thread1466 </DropdownMenuItem>
1467 )}
1468 </>
1469 );
1470}
1471
1472/** The thread panel's "⋯", in its header. */
1473function ThreadMenu({ onCopyLink, onWriteUp }: { onCopyLink: () => void; onWriteUp?: () => void }) {
1474 return (
1475 <DropdownMenu>
1476 <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.1477 <DropdownMenuTrigger asChild>
1478 <Button variant="ghost" size="icon-lg" aria-label="Thread actions" className="rounded-full active:bg-raised lg:size-7 lg:rounded-md">
1479 <Ellipsis size={16} />
1480 </Button>
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1481 </DropdownMenuTrigger>
1482 </Hint>
1483 <DropdownMenuContent align="end">
1484 <ThreadMenuItems onCopyLink={onCopyLink} onWriteUp={onWriteUp} />
1485 </DropdownMenuContent>
1486 </DropdownMenu>
1487 );
1488}
1489
Chat and workspace agents: channels, DMs and named agents you talk to1490/** "reviewer is typing…", for whoever is. */
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)1491/** What an agent working on a reply says it is doing: one phrase per agent, so each sounds like itself. */
1492const AGENT_DOING = ["is reading the thread", "is thinking it through", "is looking into it", "is reading the diff", "is checking the details", "is drafting a reply"];
1493
1494function agentDoing(id: string): string {
1495 let hash = 0;
1496 for (const char of id) hash = (hash * 31 + char.charCodeAt(0)) | 0;
1497 return AGENT_DOING[Math.abs(hash) % AGENT_DOING.length]!;
1498}
1499
Chat and workspace agents: channels, DMs and named agents you talk to1500function TypingLine({ members }: { members: MemberProfile[] }) {
1501 const names = members.map(shownName);
1502 const who = names.length === 1 ? names[0] : names.length === 2 ? `${names[0]} and ${names[1]}` : names.length > 2 ? "Several people" : "";
1503 return (
1504 <div aria-live="polite" className="flex h-6 items-center gap-1.5 px-1 text-xs text-muted">
1505 {who && (
1506 <>
1507 <span aria-hidden="true" className="flex gap-0.5">
1508 {[0, 1, 2].map((i) => (
1509 <span key={i} className="size-1 animate-bounce rounded-full bg-accent" style={{ animationDelay: `${i * 140}ms` }} />
1510 ))}
1511 </span>
1512 <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)1513 <span className="font-medium text-fg-soft">{who}</span>{" "}
1514 {names.length === 1 ? (members[0]!.kind === "agent" ? agentDoing(members[0]!.id) : "is typing") : "are typing"}…
Chat and workspace agents: channels, DMs and named agents you talk to1515 </span>
1516 </>
1517 )}
1518 </div>
1519 );
1520}
1521
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)1522/**
1523 * A panel beside the conversation: a thread, or the details. On a phone it
1524 * is a screen of its own, pushed over the conversation, as tall as what
1525 * the keyboard leaves.
1526 */
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1527function 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)1528 const swipe = useSwipeBack(onClose);
Chat and workspace agents: channels, DMs and named agents you talk to1529 return (
1530 <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)1531 {...swipe}
Chat and workspace agents: channels, DMs and named agents you talk to1532 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)1533 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 to1534 >
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)1535 <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.1536 <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)1537 <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.1538 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to1539 <h2 className="text-[0.9375rem] font-semibold">{title}</h2>
1540 {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 thread1541 <div className="ml-auto flex shrink-0 items-center gap-1">
1542 {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.1543 <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 thread1544 <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.1545 </Button>
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1546 </div>
Chat and workspace agents: channels, DMs and named agents you talk to1547 </div>
1548 <div className="flex min-h-0 grow flex-col">{children}</div>
1549 </aside>
1550 );
1551}
1552
1553function ThreadPanel({
1554 root,
1555 replies,
1556 zone,
1557 context,
1558 code,
1559 slug,
1560 people,
1561 joined,
1562 onSend,
1563 onRetry,
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1564 onTyping,
Chat and workspace agents: channels, DMs and named agents you talk to1565 draftKey,
1566}: {
1567 root: ShownMessage | null;
1568 replies: ShownMessage[] | null;
1569 zone: string | undefined;
1570 context: TextContext;
1571 code: boolean;
1572 slug: string;
1573 people: Mentionable[];
1574 joined: boolean;
1575 onSend: (body: string) => void;
1576 onRetry: (message: ShownMessage) => void;
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1577 /** Says "is typing" to the others in the channel, as the main composer does. */
1578 onTyping: () => void;
Chat and workspace agents: channels, DMs and named agents you talk to1579 draftKey: string;
1580}) {
1581 const rows = useMemo(() => timeline(replies ?? [], new Date(), zone).filter((row) => row.kind === "message"), [replies, zone]);
1582 return (
1583 <>
1584 <div className="min-h-0 grow overflow-y-auto px-1 py-3 [scrollbar-width:thin]">
1585 {root ? (
1586 <MessageRow message={root} head zone={zone} context={context} code={code} slug={slug} onRetry={() => onRetry(root)} inThread />
1587 ) : (
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1588 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 to1589 )}
1590 <div className="my-2 flex items-center gap-3 px-4 text-xs text-faint">
1591 {replies == null ? "Loading replies…" : `${replies.filter((r) => !r.deleted_at).length} ${replies.length === 1 ? "reply" : "replies"}`}
1592 <span className="h-px grow bg-line" />
1593 </div>
1594 {rows.map((row) =>
1595 row.kind === "message" ? (
1596 <MessageRow
1597 key={row.key}
1598 message={row.message}
1599 head={row.head}
1600 zone={zone}
1601 context={context}
1602 code={code}
1603 slug={slug}
1604 onRetry={() => onRetry(row.message)}
1605 inThread
1606 />
1607 ) : null,
1608 )}
1609 </div>
1610 {joined && (
1611 <div className="shrink-0 p-3">
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1612 <Composer draftKey={draftKey} placeholder="Reply…" people={people} onSend={onSend} onTyping={onTyping} compact autoFocus />
1613 </div>
1614 )}
1615 </>
1616 );
1617}
1618
1619/**
1620 * A channel's name, topic and visibility, and changing them: any member
1621 * edits the topic; renaming and archiving are for whoever the workspace's
1622 * chat settings allow (`canManage`). #general keeps its name and stays.
1623 */
1624function ChannelAbout({
1625 channel,
1626 joined,
1627 canManage,
1628 onUpdate,
1629}: {
1630 channel: Channel;
1631 joined: boolean;
1632 canManage: boolean;
1633 onUpdate: (change: ChannelChange) => Promise<Result<unknown>>;
1634}) {
1635 const [editing, setEditing] = useState<"name" | "topic" | null>(null);
1636 const [value, setValue] = useState("");
1637 const [error, setError] = useState<string | null>(null);
1638 const [busy, setBusy] = useState(false);
1639 const [confirm, setConfirm] = useState(false);
1640 const general = channel.name === "general";
1641 const archived = !!channel.archived_at;
1642 const mayRename = canManage && !general && !archived;
1643 const mayTopic = joined && !archived;
1644 const save = async (change: ChannelChange) => {
1645 setBusy(true);
1646 setError(null);
1647 const done = await onUpdate(change);
1648 setBusy(false);
1649 if (!done.ok) return setError(done.error.message);
1650 setEditing(null);
1651 setConfirm(false);
1652 };
1653 const start = (field: "name" | "topic") => {
1654 setEditing(field);
1655 setValue(field === "name" ? (channel.name ?? "") : (channel.topic ?? ""));
1656 setError(null);
1657 };
1658 const edit = (field: "name" | "topic") => (
1659 <form
1660 className="mt-1.5 space-y-2"
1661 onSubmit={(event) => {
1662 event.preventDefault();
1663 void save(field === "name" ? { name: value } : { topic: value });
1664 }}
1665 >
1666 <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">
1667 {field === "name" && (channel.private ? <Lock size={13} className="text-faint" /> : <Hash size={13} className="text-faint" />)}
1668 <input
1669 autoFocus
1670 value={value}
1671 onChange={(event) => setValue(event.target.value)}
1672 onKeyDown={(event) => event.key === "Escape" && setEditing(null)}
1673 maxLength={field === "name" ? 80 : 250}
1674 placeholder={field === "name" ? "channel-name" : "What this channel is for"}
1675 aria-label={field === "name" ? "Channel name" : "Topic"}
1676 autoComplete="off"
1677 data-1p-ignore
1678 className="min-w-0 grow bg-transparent text-sm outline-none placeholder:text-faint"
1679 />
1680 </span>
1681 <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.1682 <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 project1683 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.1684 </Button>
1685 <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 project1686 {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.1687 </Button>
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1688 </div>
1689 </form>
1690 );
1691 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.1692 <Button
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1693 type="button"
1694 onClick={() => start(field)}
1695 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.1696 variant="ghost"
1697 size="inline"
1698 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 project1699 >
1700 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.1701 </Button>
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1702 );
1703 return (
1704 <>
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.1705 <Card asChild className="group/about p-3.5">
1706 <section>
1707 <div className="flex items-center justify-between">
1708 <h3 className="text-xs font-medium text-faint">Name</h3>
1709 {mayRename && editing !== "name" && editButton("name", "Rename channel")}
1710 </div>
1711 {editing === "name" ? edit("name") : <p className="mt-1 text-sm text-fg-soft">#{channel.name}</p>}
1712 <div className="mt-3 flex items-center justify-between">
1713 <h3 className="text-xs font-medium text-faint">Topic</h3>
1714 {mayTopic && editing !== "topic" && editButton("topic", "Edit topic")}
1715 </div>
1716 {editing === "topic" ? edit("topic") : <p className="mt-1 text-sm text-fg-soft">{channel.topic || "No topic yet."}</p>}
1717 <h3 className="mt-3 text-xs font-medium text-faint">Visibility</h3>
1718 <p className="mt-1 flex items-center gap-1.5 text-sm text-fg-soft">
1719 {channel.private ? <Lock size={13} /> : <Hash size={13} />}
1720 {channel.private ? "Private: only its members can find and read it" : "Public: anyone in the workspace can read and join"}
1721 </p>
1722 {error && <p className="mt-2 text-xs text-danger">{error}</p>}
1723 </section>
1724 </Card>
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1725 {canManage && !general && (
1726 <button
1727 type="button"
1728 onClick={() => (archived ? void save({ archived: false }) : setConfirm(true))}
1729 disabled={busy}
1730 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"
1731 >
1732 {archived ? <ArchiveRestore size={14} /> : <Archive size={14} />}
1733 {archived ? "Unarchive channel" : "Archive channel"}
1734 </button>
Chat and workspace agents: channels, DMs and named agents you talk to1735 )}
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1736 <AlertDialog open={confirm} onOpenChange={setConfirm}>
1737 <AlertDialogContent>
1738 <AlertDialogHeader>
1739 <AlertDialogTitle>Archive #{channel.name}?</AlertDialogTitle>
1740 <AlertDialogDescription>
1741 It leaves everyone&apos;s sidebar and nobody can post in it. Its history stays readable, and it can be unarchived from Browse
1742 channels, Archived.
1743 </AlertDialogDescription>
1744 </AlertDialogHeader>
1745 <AlertDialogFooter>
1746 <AlertDialogCancel>Cancel</AlertDialogCancel>
1747 <AlertDialogAction
1748 onClick={(event) => {
1749 event.preventDefault();
1750 void save({ archived: true });
1751 }}
1752 disabled={busy}
1753 >
1754 {busy ? "Archiving…" : "Archive"}
1755 </AlertDialogAction>
1756 </AlertDialogFooter>
1757 </AlertDialogContent>
1758 </AlertDialog>
Chat and workspace agents: channels, DMs and named agents you talk to1759 </>
1760 );
1761}
1762
1763function InfoPanel({
1764 data,
1765 members,
1766 slug,
1767 people,
1768 onAdded,
1769 joined,
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1770 canManage,
1771 onUpdate,
Chat and workspace agents: channels, DMs and named agents you talk to1772}: {
1773 data: Loaded;
1774 members: ChannelMember[];
1775 slug: string;
1776 people: Mentionable[];
1777 onAdded: (member: ChannelMember) => void;
1778 joined: boolean;
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1779 /** Whether the viewer may rename, archive and unarchive it. */
1780 canManage: boolean;
1781 onUpdate: (change: ChannelChange) => Promise<Result<unknown>>;
Chat and workspace agents: channels, DMs and named agents you talk to1782}) {
1783 const channel = data.channel;
1784 const agents = members.filter((m) => m.member.kind === "agent");
1785 const humans = members.filter((m) => m.member.kind === "user");
1786 return (
1787 <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 project1788 {channel.kind === "channel" && <ChannelAbout channel={channel} joined={joined} canManage={canManage} onUpdate={onUpdate} />}
Chat and workspace agents: channels, DMs and named agents you talk to1789 {agents.length > 0 && (
1790 <section className="mt-5">
1791 <h3 className="mb-2 px-1 text-xs font-medium text-faint">Agents · {agents.length}</h3>
1792 <div className="flex flex-wrap gap-1.5">
1793 {agents.map(({ member }) => (
1794 <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)1795 <MemberCard
1796 member={member}
Chat and workspace agents: channels, DMs and named agents you talk to1797 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"
1798 >
1799 <MemberAvatar member={member} size={20} />
1800 {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)1801 </MemberCard>
Chat and workspace agents: channels, DMs and named agents you talk to1802 </Hint>
1803 ))}
1804 </div>
1805 </section>
1806 )}
1807 <section className="mt-5">
1808 <div className="mb-1 flex items-center justify-between px-1">
1809 <h3 className="text-xs font-medium text-faint">People · {humans.length}</h3>
1810 {joined && channel.kind === "channel" && <AddMember slug={slug} channelId={channel.id} people={people} members={members} onAdded={onAdded} />}
1811 </div>
1812 <ul className="space-y-px">
1813 {humans.map(({ member, role }) => (
1814 <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)1815 <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 managers1816 <MemberAvatar member={member} size={26} presence />
Chat and workspace agents: channels, DMs and named agents you talk to1817 <span className="min-w-0 grow truncate text-sm">
1818 {shownName(member)}
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar1819 {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 managers1820 <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 to1821 </span>
1822 {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)1823 </MemberCard>
Chat and workspace agents: channels, DMs and named agents you talk to1824 </li>
1825 ))}
1826 </ul>
1827 </section>
1828 </div>
1829 );
1830}
1831
1832/** Adding a person or an agent to the channel. An invite lets them read; it never grants write. */
1833function AddMember({
1834 slug,
1835 channelId,
1836 people,
1837 members,
1838 onAdded,
1839}: {
1840 slug: string;
1841 channelId: string;
1842 people: Mentionable[];
1843 members: ChannelMember[];
1844 onAdded: (member: ChannelMember) => void;
1845}) {
1846 const [open, setOpen] = useState(false);
1847 const [query, setQuery] = useState("");
1848 const [error, setError] = useState<string | null>(null);
1849 const send = useChatSend(slug);
1850 const chatData = useChatData();
1851 const inside = new Set(members.map((m) => `${m.member.kind}:${m.member.name.toLowerCase()}`));
1852 const q = query.trim().toLowerCase().replace(/^@/, "");
1853 const options = people
1854 .filter((p) => !inside.has(`${p.kind}:${p.name.toLowerCase()}`))
1855 .filter((p) => !q || p.name.toLowerCase().includes(q) || p.display_name.toLowerCase().includes(q))
1856 .slice(0, 8);
1857 const add = async (person: Mentionable) => {
1858 const agent = person.kind === "agent" ? chatData?.agents.find((a) => a.handle === person.name) : null;
1859 const key = agent ? `agent:${agent.id}` : `user:${person.name}`;
1860 setError(null);
1861 const done = await send({ intent: "invite", channel_id: channelId, member: key });
1862 if (!done.ok) return setError(done.error.message);
1863 onAdded({
1864 channel_id: channelId,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar1865 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 to1866 role: "member",
1867 starred: false,
1868 muted: false,
1869 last_read_id: null,
1870 joined_at: new Date().toISOString(),
1871 });
1872 setOpen(false);
1873 };
1874 return (
1875 <Popover open={open} onOpenChange={setOpen}>
1876 <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.1877 <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 to1878 <UserPlus size={13} />
1879 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.1880 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to1881 </PopoverTrigger>
1882 <PopoverContent align="end" className="w-72 p-1.5">
1883 <input
1884 autoFocus
1885 value={query}
1886 onChange={(event) => setQuery(event.target.value)}
1887 placeholder="Add a person or an agent"
1888 aria-label="Add a person or an agent"
1889 autoComplete="off"
1890 data-1p-ignore
1891 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"
1892 />
1893 {options.length === 0 && <p className="px-2 py-3 text-xs text-faint">Everyone who matches is already here.</p>}
1894 {options.map((person) => (
1895 <button
1896 key={`${person.kind}:${person.name}`}
1897 type="button"
1898 onClick={() => void add(person)}
1899 className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm hover:bg-line"
1900 >
1901 <MemberAvatar member={person} size={20} />
1902 <span className="min-w-0 grow truncate">{person.display_name}</span>
1903 {person.kind === "agent" && <AgentPill />}
1904 </button>
1905 ))}
1906 {error && <p className="px-2 pt-1 text-xs text-danger">{error}</p>}
1907 <p className="border-t border-line px-2 pt-1.5 pb-0.5 text-[0.6875rem] leading-snug text-faint">
1908 Adding an agent lets it read this channel. It never grants it write access.
1909 </p>
1910 </PopoverContent>
1911 </Popover>
1912 );
1913}

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