Skip to content
1,821 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";
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers41import { 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)42import { 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.43
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)44import { localTime } from "../../lib/time-zone";
45import { BottomSheet, SheetRow, useBack, useSwipeBack } from "../mobile";
Chat and workspace agents: channels, DMs and named agents you talk to46import { 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 thread47import { 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.48import { 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 thread49import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../ui/dropdown-menu";
50import { threadLink, writeUpAgents } from "../../lib/write-up";
Chat controls, public profiles, shadcn selects, and no Docs tab in a project51import {
52 AlertDialog,
53 AlertDialogAction,
54 AlertDialogCancel,
55 AlertDialogContent,
56 AlertDialogDescription,
57 AlertDialogFooter,
58 AlertDialogHeader,
59 AlertDialogTitle,
60} 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.61import { Avatar } from "../ui/avatar";
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar62import { Badge } from "../ui/badge";
Chat and workspace agents: channels, DMs and named agents you talk to63import { 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.64import { 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.65import { Card } from "../ui/card";
Chat and workspace agents: channels, DMs and named agents you talk to66import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar67import { type Mentionable, type ShownMessage, channelPath, mentionNames, mergeMessages, shownHandle, shownName, timeline } from "../../lib/chat";
Chat and workspace agents: channels, DMs and named agents you talk to68import { 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)69import { conversationCache } from "./conversation-cache";
70// Reactions and the workspace's own emoji (components/emoji).
71import { EmojiProvider, useCustomEmoji } from "../emoji/context";
72import { AddReaction, QuickReactions, ReactionBar } from "../emoji/reaction-bar";
73import { useAddresses } from "../../lib/addresses";
74import { applyReactionEvent, keepMine, toggleReaction } from "../../lib/emoji";
Chat and workspace agents: channels, DMs and named agents you talk to75import type { ChannelData } from "../../routes/workspace/chat/channel";
76
77type Loaded = Extract<ChannelData, { unavailable: false }>;
78
79/** True once the page has hydrated: times and days are the reader's own from then on. */
80function useHydrated(): boolean {
81 const [hydrated, setHydrated] = useState(false);
82 useEffect(() => setHydrated(true), []);
83 return hydrated;
84}
85
86function clock(at: string, zone: string | undefined): string {
87 return new Date(at).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", timeZone: zone });
88}
89
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.90/** Whether the viewer uses Code in this workspace. */
Chat and workspace agents: channels, DMs and named agents you talk to91export function useCodeAccess(slug: string): boolean {
92 const root = useRouteLoaderData("root") as { user?: { workspaces?: { slug: string; code_access?: boolean }[] } | null } | undefined;
93 return hasCodeAccess(root?.user?.workspaces?.find((m) => m.slug === slug) ?? null);
94}
95
96/** The project a channel of the same name is about, for a bare `#123`. */
97function useProjectNamed(name: string | null): string | null {
98 const root = useRouteLoaderData("root") as { shell?: { repos?: { name: string }[] } } | undefined;
99 if (!name) return null;
100 return root?.shell?.repos?.some((repo) => repo.name === name) ? name : null;
101}
102
103function newClientId(): string {
104 return `tmp-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
105}
106
107/** A conversation: its header, its messages, the box to write in, and its thread and details beside it. */
108export function ChannelView({ data }: { data: Loaded }) {
109 const chatData = useChatData();
110 const slug = chatData?.slug ?? "";
111 const me = chatData?.me ?? null;
112 const send = useChatSend(slug);
113 const { sidebar, refresh } = useChatSidebar();
114 const hydrated = useHydrated();
115 const zone = hydrated ? undefined : "UTC";
116 const code = useCodeAccess(slug);
117 const project = useProjectNamed(data.channel.kind === "channel" ? data.channel.name : null);
118 const [params, setParams] = useSearchParams();
119 const threadId = params.get("thread");
120
121 const [messages, setMessages] = useState<ShownMessage[]>(() => mergeMessages([], data.messages));
122 const [older, setOlder] = useState<string | null>(data.older);
123 const [loadingOlder, setLoadingOlder] = useState(false);
124 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 thread125 // The message the open thread is under, as its page says, for when it is
126 // further back than the conversation has loaded (a long-running session's card).
127 const [threadRoot, setThreadRoot] = useState<ShownMessage | null>(null);
Chat and workspace agents: channels, DMs and named agents you talk to128 const [typing, setTyping] = useState<Map<string, { member: MemberProfile; until: number }>>(new Map());
129 const [joined, setJoined] = useState(data.joined);
130 const [members, setMembers] = useState<ChannelMember[]>(data.members);
Chat controls, public profiles, shadcn selects, and no Docs tab in a project131 // The channel as it is now: renamed, its topic changed, archived, live.
132 const [channel, setChannel] = useState<Channel>(data.channel);
133 const [canManage, setCanManage] = useState(data.can_manage === true);
134 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)135 // The server's answer behind a cached draw: merged in by id.
136 useEffect(() => {
137 const onFresh = (event: Event) => {
138 const { key, value } = (event as CustomEvent<{ key: string; value: ChannelData }>).detail;
139 if (key !== window.location.pathname || value.unavailable || value.channel.id !== data.channel.id) return;
140 setMessages((now) => mergeMessages(now, value.messages));
141 setMembers(value.members);
142 setJoined(value.joined);
Chat controls, public profiles, shadcn selects, and no Docs tab in a project143 setChannel(value.channel);
144 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)145 };
146 window.addEventListener("g1t:chat-fresh", onFresh);
147 return () => window.removeEventListener("g1t:chat-fresh", onFresh);
148 }, [data.channel.id]);
149 // The cache follows what is on screen, so coming back shows the latest.
150 useEffect(() => {
151 const path = window.location.pathname;
152 const timer = setTimeout(
Chat controls, public profiles, shadcn selects, and no Docs tab in a project153 () => 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)154 400,
155 );
156 return () => clearTimeout(timer);
Chat controls, public profiles, shadcn selects, and no Docs tab in a project157 }, [view, messages, members, joined, canManage]);
158 // Renamed while open, here or by someone else: the address follows the name.
159 const navigate = useNavigate();
160 const nameShown = useRef(data.channel.name);
161 useEffect(() => {
162 if (channel.kind !== "channel" || channel.name === nameShown.current || !slug) return;
163 nameShown.current = channel.name;
164 navigate(`${channelPath(slug, channel)}${window.location.search}`, { replace: true, preventScrollReset: true });
165 }, [channel, slug, navigate]);
Chat and workspace agents: channels, DMs and named agents you talk to166 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)167 // A phone: back to the list, by the chevron or a swipe from the edge;
168 // and a long-pressed message's actions.
169 const back = useBack(`/${chatData?.slug ?? ""}/-/chat`);
170 const swipe = useSwipeBack(back);
171 const [actions, setActions] = useState<ShownMessage | null>(null);
172 // Someone's profile, opened from their card, beside the conversation.
173 const [profile, setProfile] = useState<string | null>(null);
Merge main into Artifacts Phase 2174 // A message being edited: in its place on a computer (`inline`), in a sheet on a phone.
175 const [editing, setEditing] = useState<{ id: string; body: string; inline?: boolean } | null>(null);
Chat and workspace agents: channels, DMs and named agents you talk to176 const scroller = useRef<HTMLDivElement>(null);
177 const stick = useRef(true);
178
179 const entry = sidebar?.entries.find((e) => e.channel.id === data.channel.id);
180 const starred = entry?.starred ?? members.find((m) => m.member.kind === "user" && m.member.id === me?.id)?.starred ?? false;
181 const muted = entry?.muted ?? false;
182 const others = members.filter((m) => !(m.member.kind === "user" && m.member.id === me?.id)).map((m) => m.member);
183 const isDm = data.channel.kind === "dm";
184
185 // The details panel: open by default on a wide screen, as last left.
186 useEffect(() => {
187 try {
188 const saved = localStorage.getItem("chat-info");
189 setInfo(saved ? saved === "1" : window.matchMedia("(min-width: 1280px)").matches && !isDm);
190 } catch {
191 setInfo(false);
192 }
193 }, [isDm]);
194 const toggleInfo = () =>
195 setInfo((now) => {
196 try {
197 localStorage.setItem("chat-info", now ? "0" : "1");
198 } catch {
199 // Not kept: fine.
200 }
201 return !now;
202 });
203
204 const people = useMemo<Mentionable[]>(
205 () => [
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.206 // A personal agent is never mentioned: it answers only in its own DM.
207 ...(chatData?.agents ?? [])
208 .filter((agent) => agent.scope !== "personal")
209 .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 to210 ...(chatData?.people ?? []).filter((p) => p.name !== me?.username),
211 ],
212 [chatData, me?.username],
213 );
214 const context = useMemo<TextContext>(
215 () => ({
216 slug,
217 me: me?.username ?? null,
218 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 calendar219 // `@ana` reads as the name people know: everyone in the workspace, the viewer too.
220 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 to221 channels: new Set((sidebar?.entries ?? []).filter((e) => e.channel.kind === "channel").map((e) => e.channel.name ?? "")),
222 project,
223 codeLink: code ? undefined : (href: string) => (/^\/[^/]+\/(?!-\/)[^/]+/.test(href) ? codeAccessPath(slug, href) : href),
224 }),
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar225 [slug, me, people, chatData?.agents, sidebar?.entries, project, code],
Chat and workspace agents: channels, DMs and named agents you talk to226 );
227
228 // Keeps the newest message in view while the reader is at the bottom.
229 const toBottom = useCallback((smooth = false) => {
230 const element = scroller.current;
231 if (element) element.scrollTo({ top: element.scrollHeight, behavior: smooth ? "smooth" : "auto" });
232 }, []);
233 useEffect(() => toBottom(), [toBottom]);
234 useEffect(() => {
235 if (stick.current) toBottom(true);
236 }, [messages.length, typing.size, toBottom]);
237
238 // Read up to the newest message whenever it is in view.
239 const lastRead = useRef<string | null>(null);
240 const markRead = useCallback(() => {
241 if (!joined || document.visibilityState !== "visible") return;
242 const newest = [...messages].reverse().find((m) => !m.pending);
243 if (!newest || newest.id === lastRead.current) return;
244 lastRead.current = newest.id;
245 void send({ intent: "read", channel_id: data.channel.id, id: newest.id });
246 window.dispatchEvent(new CustomEvent("g1t:chat-read", { detail: data.channel.id }));
247 }, [messages, joined, send, data.channel.id]);
248 useEffect(() => {
249 const timer = setTimeout(markRead, 400);
250 document.addEventListener("visibilitychange", markRead);
251 window.addEventListener("focus", markRead);
252 return () => {
253 clearTimeout(timer);
254 document.removeEventListener("visibilitychange", markRead);
255 window.removeEventListener("focus", markRead);
256 };
257 }, [markRead]);
258
259 // The thread open beside the conversation, from `?thread=`.
260 const loadThread = useCallback(async () => {
261 if (!threadId) return setThread(null);
262 try {
263 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 thread264 const result = (await response.json()) as Result<{ messages: ChatMessage[]; root?: ChatMessage | null }>;
Chat and workspace agents: channels, DMs and named agents you talk to265 // The page reaches back to the message the thread is under: shown above it, not among the replies.
266 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 thread267 const root = result.ok ? (result.value.root ?? result.value.messages.find((m) => m.id === threadId) ?? null) : null;
268 setThreadRoot(root);
Chat and workspace agents: channels, DMs and named agents you talk to269 } catch {
270 setThread([]);
271 }
272 }, [threadId, slug, data.channel.id]);
273 useEffect(() => {
274 setThread(null);
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread275 setThreadRoot(null);
Chat and workspace agents: channels, DMs and named agents you talk to276 void loadThread();
277 }, [loadThread]);
278
279 // Reply counts: counted here only until the service's own count for that
280 // message arrives (a `message.updated` of it), which can come before or
281 // after the reply itself; never twice for one reply.
282 const serverCounted = useRef(new Set<string>());
283 const countedReplies = useRef(new Set<string>());
284 const countReply = useCallback((root: string, reply: string, at: string) => {
285 if (serverCounted.current.has(root) || countedReplies.current.has(reply)) return;
286 countedReplies.current.add(reply);
287 setMessages((now) => now.map((m) => (m.id === root ? { ...m, reply_count: m.reply_count + 1, last_reply_at: at } : m)));
288 }, []);
289 const latest = useRef(messages);
290 latest.current = messages;
291
292 // Everything that happens while the page is open.
293 const onEvent = useCallback(
294 (event: ChatLiveEvent) => {
295 if (event.type === "message.created" || event.type === "message.updated") {
296 const message = event.message;
297 if (message.channel_id !== data.channel.id) return;
298 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)299 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 to300 if (event.type === "message.created") countReply(message.thread_root, message.id, message.created_at);
301 } else {
302 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)303 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 thread304 // The open thread's root, a session's card say, changes in place at its top too.
305 if (message.id === threadId) setThreadRoot((now) => (now ? keepMine([now], message) : now));
Chat and workspace agents: channels, DMs and named agents you talk to306 }
307 // Whoever just spoke is no longer typing.
308 setTyping((now) => {
309 const key = `${message.author.kind}:${message.author.id}`;
310 if (!now.has(key)) return now;
311 const next = new Map(now);
312 next.delete(key);
313 return next;
314 });
315 } else if (event.type === "message.deleted") {
316 if (event.channel_id !== data.channel.id) return;
317 const gone = (m: ShownMessage) => (m.id === event.id ? { ...m, deleted_at: new Date().toISOString() } : m);
318 setMessages((now) => now.map(gone));
319 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 thread320 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)321 } else if (event.type === "reaction.added" || event.type === "reaction.removed") {
322 if (event.channel_id !== data.channel.id) return;
323 setMessages((now) => applyReactionEvent(now, event, me?.id ?? null));
324 setThread((now) => (now ? applyReactionEvent(now, event, me?.id ?? null) : now));
Chat and workspace agents: channels, DMs and named agents you talk to325 } else if (event.type === "typing") {
326 if (event.channel_id !== data.channel.id) return;
327 if (event.member.kind === "user" && event.member.id === me?.id) return;
328 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 project329 } else if (event.type === "channel.updated") {
330 // Renamed, a new topic, archived or back: everyone looking sees it at once.
331 if (event.channel.id !== data.channel.id) return;
332 setChannel(event.channel);
333 refresh();
Chat and workspace agents: channels, DMs and named agents you talk to334 }
335 },
Chat controls, public profiles, shadcn selects, and no Docs tab in a project336 [data.channel.id, threadId, me?.id, countReply, refresh],
Chat and workspace agents: channels, DMs and named agents you talk to337 );
338 // Back after a drop: what was missed.
339 const onReconnect = useCallback(async () => {
340 const base = `/${slug}/-/chat/api?channel=${encodeURIComponent(data.channel.id)}`;
341 try {
342 // Everything after the newest message held, page by page, oldest first;
343 // what was deleted meanwhile comes back empty and is dropped.
344 let after = [...latest.current].reverse().find((m) => !m.pending)?.id ?? null;
345 for (let pages = 0; after && pages < 20; pages++) {
346 const response = await fetch(`${base}&after=${encodeURIComponent(after)}`);
347 const result = (await response.json()) as Result<{ messages: ChatMessage[]; newer?: string | null }>;
348 if (!result.ok) break;
349 const gone = new Set(result.value.messages.filter((m) => m.deleted_at).map((m) => m.id));
350 const kept = result.value.messages.filter((m) => !m.deleted_at && !m.thread_root);
351 setMessages((now) => mergeMessages(now.filter((m) => !gone.has(m.id)), kept));
352 after = result.value.newer ?? null;
353 }
354 // Edits and deletions to what was already on screen: the latest page again.
355 const response = await fetch(base);
356 const result = (await response.json()) as Result<{ messages: ChatMessage[] }>;
357 if (result.ok) {
358 const gone = new Set(result.value.messages.filter((m) => m.deleted_at).map((m) => m.id));
359 setMessages((now) => mergeMessages(now.filter((m) => !gone.has(m.id)), result.value.messages.filter((m) => !m.deleted_at)));
360 }
361 } catch {
362 // The next event, or the next return, catches up.
363 }
364 void loadThread();
365 refresh();
366 }, [slug, data.channel.id, loadThread, refresh]);
367 const live = useChatLive(slug, data.channel.id, onEvent, onReconnect);
368
369 // Typing shows until its time runs out.
370 useEffect(() => {
371 if (typing.size === 0) return;
372 const timer = setInterval(() => {
373 const now = Date.now();
374 setTyping((current) => {
375 const next = new Map([...current].filter(([, value]) => value.until > now));
376 return next.size === current.size ? current : next;
377 });
378 }, 1000);
379 return () => clearInterval(timer);
380 }, [typing.size]);
381
382 // Says "is typing" to the others, at most every few seconds.
383 const lastTyping = useRef(0);
384 const onTyping = useCallback(() => {
385 const now = Date.now();
386 if (now - lastTyping.current < 3000) return;
387 lastTyping.current = now;
388 live.send({ type: "typing", channel_id: data.channel.id });
389 }, [live, data.channel.id]);
390
391 const post = useCallback(
392 async (body: string, root: string | null) => {
393 if (!me) return;
394 const clientId = newClientId();
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar395 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 to396 const pending: ShownMessage = {
397 id: clientId,
398 client_id: clientId,
399 pending: true,
400 channel_id: data.channel.id,
401 author,
402 kind: "text",
403 body,
404 card: null,
405 thread_root: root,
406 reply_count: 0,
407 last_reply_at: null,
408 created_at: new Date().toISOString(),
409 edited_at: null,
410 deleted_at: null,
411 };
412 const put = (list: ShownMessage[]) => mergeMessages(list, [pending]);
413 if (root) setThread((now) => put(now ?? []));
414 else {
415 stick.current = true;
416 setMessages(put);
417 }
418 const saved = await send<ChatMessage>({ intent: "post", channel_id: data.channel.id, body, thread_root: root });
419 const settle = (list: ShownMessage[]) =>
420 saved.ok
421 ? mergeMessages(list, [{ ...saved.value, client_id: clientId }])
422 : list.map((m) => (m.client_id === clientId ? { ...m, failed: true, pending: true } : m));
423 if (root) {
424 setThread((now) => (now ? settle(now) : now));
425 if (saved.ok) countReply(root, saved.value.id, saved.value.created_at);
426 } else setMessages(settle);
427 },
428 [me, send, data.channel.id, countReply],
429 );
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)430 // ── Reactions (components/emoji): shown at once, then as the service counts them.
431 const customs = useCustomEmoji(slug);
432 const { usercontent } = useAddresses();
433 const meProfile = useMemo<MemberProfile | null>(
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar434 () => (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)435 [me],
436 );
437 const react = useCallback(
438 async (messageId: string, emoji: string, on: boolean) => {
439 if (!meProfile) return;
440 const change = (to: boolean) => (list: ShownMessage[]) =>
441 list.map((m) => (m.id === messageId ? { ...m, reactions: toggleReaction(m.reactions ?? [], emoji, to, meProfile) } : m));
442 setMessages(change(on));
443 setThread((now) => (now ? change(on)(now) : now));
444 try {
445 const response = await fetch(`/${slug}/-/chat/api`, {
446 method: "POST",
447 headers: { "content-type": "application/json" },
448 body: JSON.stringify({ intent: on ? "react" : "unreact", channel_id: data.channel.id, message_id: messageId, emoji }),
449 });
450 const result = (await response.json()) as Result<ChatMessage["reactions"]>;
451 if (!result.ok) throw new Error(result.error.message);
452 const settle = (list: ShownMessage[]) => list.map((m) => (m.id === messageId ? { ...m, reactions: result.value } : m));
453 setMessages(settle);
454 setThread((now) => (now ? settle(now) : now));
455 } catch {
456 // Not saved: back as it was.
457 setMessages(change(!on));
458 setThread((now) => (now ? change(!on)(now) : now));
459 }
460 },
461 [meProfile, slug, data.channel.id],
462 );
463 const emojiContext = useMemo(
464 () => ({ customs, usercontent, me: meProfile, react: (id: string, emoji: string, on: boolean) => void react(id, emoji, on), manageHref: `/${slug}/-/emoji` }),
465 [customs, usercontent, meProfile, react, slug],
466 );
467
Chat and workspace agents: channels, DMs and named agents you talk to468 const retry = (message: ShownMessage) => {
469 const drop = (list: ShownMessage[]) => list.filter((m) => m.client_id !== message.client_id);
470 if (message.thread_root) setThread((now) => (now ? drop(now) : now));
471 else setMessages(drop);
472 void post(message.body, message.thread_root);
473 };
474
475 const loadOlder = async () => {
476 if (!older || loadingOlder) return;
477 setLoadingOlder(true);
478 const element = scroller.current;
479 const fromBottom = element ? element.scrollHeight - element.scrollTop : 0;
480 try {
481 const response = await fetch(`/${slug}/-/chat/api?channel=${encodeURIComponent(data.channel.id)}&before=${encodeURIComponent(older)}`);
482 const result = (await response.json()) as Result<{ messages: ChatMessage[]; older: string | null }>;
483 if (result.ok) {
484 stick.current = false;
485 setMessages((now) => mergeMessages(now, result.value.messages));
486 setOlder(result.value.older);
487 requestAnimationFrame(() => {
488 if (element) element.scrollTop = element.scrollHeight - fromBottom;
489 });
490 }
491 } finally {
492 setLoadingOlder(false);
493 }
494 };
495
496 const openThread = (id: string | null) => {
497 const next = new URLSearchParams(params);
498 if (id) next.set("thread", id);
499 else next.delete("thread");
500 setParams(next, { preventScrollReset: true, replace: !!threadId && !!id });
501 };
502
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.503 // A thread's link, to copy and to cite; and "Write this up as an artifact",
504 // 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 thread505 const [writeUp, setWriteUp] = useState<string | null>(null);
506 const linkToThread = (root: string) => threadLink(window.location.origin, channelPath(slug, channel), root);
507 const copyThreadLink = (root: string) => {
508 const done = navigator.clipboard?.writeText(linkToThread(root));
509 if (!done) return showToast(false, "Couldn't copy the link here.");
510 done.then(
511 () => showToast(true, "Link to the thread copied."),
512 () => showToast(false, "Couldn't copy the link here."),
513 );
514 };
515 const writers = useMemo(() => writeUpAgents(members.map((m) => m.member)), [members]);
516 const askForWriteUp = async (body: string): Promise<string | null> => {
517 const root = writeUp;
518 if (!root) return null;
519 // The thread open already: posted as any reply is, shown at once.
520 if (root === threadId) {
521 void post(body, root);
522 return null;
523 }
524 const saved = await send<ChatMessage>({ intent: "post", channel_id: data.channel.id, body, thread_root: root });
525 if (!saved.ok) return saved.error.message;
526 openThread(root);
527 return null;
528 };
529
Chat controls, public profiles, shadcn selects, and no Docs tab in a project530 const updateChannel = useCallback(
531 async (change: ChannelChange): Promise<Result<unknown>> => {
532 const done = await send<{ channel: Channel }>({ intent: "update_channel", channel_id: data.channel.id, ...change });
533 if (done.ok) setChannel(done.value.channel);
534 return done;
535 },
536 [send, data.channel.id],
537 );
538
Chat and workspace agents: channels, DMs and named agents you talk to539 const toggleStar = async () => {
540 await send({ intent: "preferences", channel_id: data.channel.id, starred: !starred });
541 refresh();
542 };
543
544 const join = async () => {
545 const done = await send({ intent: "join", channel_id: data.channel.id });
546 if (done.ok) {
547 setJoined(true);
548 if (me) {
549 setMembers((now) => [
550 ...now,
551 {
552 channel_id: data.channel.id,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar553 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 to554 role: "member",
555 starred: false,
556 muted: false,
557 last_read_id: null,
558 joined_at: new Date().toISOString(),
559 },
560 ]);
561 }
562 }
563 };
564
565 const rows = useMemo(() => timeline(messages, new Date(), zone), [messages, zone]);
Merge main into Artifacts Phase 2566 const mine = (message: ShownMessage) => message.author.kind === "user" && message.author.id === me?.id && !message.pending && !message.deleted_at;
567 // Editing uses the composer, started from the message's Markdown.
568 const editor = (target: { id: string; body: string }) => (
569 <Composer
570 initial={target.body}
571 placeholder="Edit message"
572 people={people}
573 autoFocus
574 compact
575 onCancel={() => setEditing(null)}
576 onSend={async (body) => {
577 setEditing(null);
578 if (body === target.body) return;
579 // Shown edited at once; put back if the service refuses.
580 const before = messages.find((m) => m.id === target.id);
581 setMessages((now) => now.map((m) => (m.id === target.id ? { ...m, body, edited_at: new Date().toISOString() } : m)));
582 const saved = await send<ChatMessage>({ intent: "edit", channel_id: data.channel.id, id: target.id, body });
583 if (saved.ok) setMessages((now) => mergeMessages(now, [saved.value]));
584 else if (before) setMessages((now) => now.map((m) => (m.id === before.id ? before : m)));
585 }}
586 />
587 );
Chat and workspace agents: channels, DMs and named agents you talk to588 const typers = [...typing.values()].map((t) => t.member);
Chat controls, public profiles, shadcn selects, and no Docs tab in a project589 const name = channel.name ?? "";
590 const archived = channel.kind === "channel" && !!channel.archived_at;
Chat and workspace agents: channels, DMs and named agents you talk to591 const placeholder = isDm
592 ? `Message ${others.map(shownName).join(", ") || "yourself"}`
593 : `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 thread594 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)595 const cardContext = useMemo(
596 () => ({
597 slug,
598 agents: chatData?.agents ?? [],
599 onViewProfile: (username: string) => {
600 if (threadId) openThread(null);
601 setProfile(username);
602 },
603 }),
604 // eslint-disable-next-line react-hooks/exhaustive-deps
605 [slug, chatData?.agents, threadId],
606 );
Chat and workspace agents: channels, DMs and named agents you talk to607
608 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)609 // On a phone the conversation is the whole screen, as tall as what the
610 // keyboard leaves (components/mobile.tsx), so the composer sits on it.
611 <EmojiProvider value={emojiContext}>
612 <CardContext.Provider value={cardContext}>
613 <div
614 {...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.615 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)616 >
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar617 {/* What pressing a card's action did (components/chat/card.tsx). */}
618 <CardToasts />
Chat and workspace agents: channels, DMs and named agents you talk to619 <section aria-label={data.title} className="flex min-w-0 grow flex-col">
620 <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)621 onBack={back}
Chat controls, public profiles, shadcn selects, and no Docs tab in a project622 data={view}
Chat and workspace agents: channels, DMs and named agents you talk to623 others={others}
624 starred={starred}
625 muted={muted}
626 memberCount={members.length}
627 live={live.state}
628 info={info}
629 onStar={() => void toggleStar()}
630 onInfo={toggleInfo}
631 />
632 <div
633 ref={scroller}
634 onScroll={(event) => {
635 const element = event.currentTarget;
636 stick.current = element.scrollHeight - element.scrollTop - element.clientHeight < 120;
637 }}
638 className="min-h-0 grow overflow-y-auto overscroll-contain [scrollbar-gutter:stable] [scrollbar-width:thin]"
639 >
640 <div className="mx-auto flex min-h-full max-w-[56rem] flex-col justify-end px-2 pt-6 pb-3 sm:px-4">
641 {older ? (
642 <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.643 <Button
Chat and workspace agents: channels, DMs and named agents you talk to644 type="button"
645 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.646 variant="outline"
647 size="inline"
648 className="rounded-full px-3 py-1 text-xs text-muted font-normal"
Chat and workspace agents: channels, DMs and named agents you talk to649 >
650 {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.651 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to652 </div>
653 ) : (
Chat controls, public profiles, shadcn selects, and no Docs tab in a project654 <ConversationStart data={view} others={others} />
Chat and workspace agents: channels, DMs and named agents you talk to655 )}
656 {rows.map((row) =>
657 row.kind === "day" ? (
658 <DayRule key={row.key} label={row.label} />
659 ) : (
660 <MessageRow
661 key={row.key}
662 message={row.message}
663 head={row.head}
664 zone={zone}
665 context={context}
666 code={code}
667 slug={slug}
668 onThread={() => openThread(row.message.id)}
669 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)670 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 thread671 onCopyLink={() => copyThreadLink(row.message.id)}
Merge main into Artifacts Phase 2672 onEdit={mine(row.message) ? () => setEditing({ id: row.message.id, body: row.message.body, inline: true }) : undefined}
673 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 thread674 onWriteUp={joined && !archived ? () => setWriteUp(row.message.id) : undefined}
Chat and workspace agents: channels, DMs and named agents you talk to675 threadOpen={row.message.id === threadId}
676 />
677 ),
678 )}
679 </div>
680 </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)681 <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 to682 <div className="mx-auto max-w-[56rem]">
683 <TypingLine members={typers} />
Chat controls, public profiles, shadcn selects, and no Docs tab in a project684 {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.685 <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 project686 <p className="flex items-center gap-2 text-sm text-muted">
687 <Archive size={15} className="shrink-0 text-faint" />
688 <span>
689 <span className="font-medium text-fg">#{name}</span> is archived. Its history stays here to read; nobody can post in it.
690 </span>
691 </p>
692 {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.693 <Button
Chat controls, public profiles, shadcn selects, and no Docs tab in a project694 type="button"
695 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.696 variant="outline"
697 className="px-3 text-fg/90 hover:bg-raised"
Chat controls, public profiles, shadcn selects, and no Docs tab in a project698 >
699 <ArchiveRestore size={15} />
700 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.701 </Button>
Chat controls, public profiles, shadcn selects, and no Docs tab in a project702 )}
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.703 </Card>
Chat controls, public profiles, shadcn selects, and no Docs tab in a project704 ) : joined ? (
Chat and workspace agents: channels, DMs and named agents you talk to705 <Composer draftKey={data.channel.id} placeholder={placeholder} people={people} onSend={(body) => void post(body, null)} onTyping={onTyping} autoFocus />
706 ) : (
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.707 <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 to708 <p className="text-sm text-muted">
709 You're reading <span className="font-medium text-fg">#{name}</span>. Join to post and get its notifications.
710 </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.711 <Button
Chat and workspace agents: channels, DMs and named agents you talk to712 type="button"
713 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.714 variant="accent"
Chat and workspace agents: channels, DMs and named agents you talk to715 >
716 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.717 </Button>
718 </Card>
Chat and workspace agents: channels, DMs and named agents you talk to719 )}
720 </div>
721 </div>
722 </section>
723 {threadId ? (
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread724 <SidePanel
725 title="Thread"
726 subtitle={isDm ? undefined : `#${name}`}
727 onClose={() => openThread(null)}
728 menu={<ThreadMenu onCopyLink={() => copyThreadLink(threadId)} onWriteUp={joined && !archived ? () => setWriteUp(threadId) : undefined} />}
729 >
Chat and workspace agents: channels, DMs and named agents you talk to730 <ThreadPanel
731 root={rootMessage}
732 replies={thread}
733 zone={zone}
734 context={context}
735 code={code}
736 slug={slug}
737 people={people}
Chat controls, public profiles, shadcn selects, and no Docs tab in a project738 joined={joined && !archived}
Chat and workspace agents: channels, DMs and named agents you talk to739 onSend={(body) => void post(body, threadId)}
740 onRetry={retry}
Chat controls, public profiles, shadcn selects, and no Docs tab in a project741 onTyping={onTyping}
Chat and workspace agents: channels, DMs and named agents you talk to742 draftKey={`${data.channel.id}:${threadId}`}
743 />
744 </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)745 ) : profile ? (
746 <SidePanel title="Profile" onClose={() => setProfile(null)}>
747 <ProfilePanel slug={slug} username={profile} messages={messages} zone={zone} context={context} />
748 </SidePanel>
Chat and workspace agents: channels, DMs and named agents you talk to749 ) : (
750 info && (
751 <SidePanel title={isDm ? "Details" : "About this channel"} onClose={toggleInfo}>
Chat controls, public profiles, shadcn selects, and no Docs tab in a project752 <InfoPanel
753 data={view}
754 members={members}
755 slug={slug}
756 people={people}
757 onAdded={(m) => setMembers((now) => [...now, m])}
758 joined={joined}
759 canManage={canManage}
760 onUpdate={updateChannel}
761 />
Chat and workspace agents: channels, DMs and named agents you talk to762 </SidePanel>
763 )
764 )}
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)765 <MessageActions
766 message={actions}
767 mine={actions != null && actions.author.kind === "user" && actions.author.id === me?.id}
768 onClose={() => setActions(null)}
769 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 thread770 onCopyLink={(message) => copyThreadLink(message.thread_root ?? message.id)}
771 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)772 onEdit={(message) => setEditing({ id: message.id, body: message.body })}
773 onDelete={async (message) => {
774 // Gone at once; back if the service refuses.
775 setMessages((now) => now.map((m) => (m.id === message.id ? { ...m, deleted_at: new Date().toISOString() } : m)));
776 const done = await send({ intent: "remove", channel_id: data.channel.id, id: message.id });
777 if (!done.ok) setMessages((now) => now.map((m) => (m.id === message.id ? { ...m, deleted_at: null } : m)));
778 }}
779 />
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread780 <WriteUpDialog
781 open={writeUp != null}
782 onOpenChange={(open) => !open && setWriteUp(null)}
783 slug={slug}
784 agents={writers}
785 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.786 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 thread787 onSend={askForWriteUp}
788 />
Merge main into Artifacts Phase 2789 <BottomSheet open={editing != null && !editing.inline} onOpenChange={(open) => !open && setEditing(null)} title="Edit message">
790 {editing && !editing.inline && (
791 <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)792 <h2 className="px-2 text-base font-semibold">Edit message</h2>
Merge main into Artifacts Phase 2793 {editor(editing)}
794 </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)795 )}
796 </BottomSheet>
Chat and workspace agents: channels, DMs and named agents you talk to797 </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)798 </CardContext.Provider>
799 </EmojiProvider>
Chat and workspace agents: channels, DMs and named agents you talk to800 );
801}
802
803/** The top of a conversation: what it is, and its controls. */
804function 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)805 onBack,
Chat and workspace agents: channels, DMs and named agents you talk to806 data,
807 others,
808 starred,
809 muted,
810 memberCount,
811 live,
812 info,
813 onStar,
814 onInfo,
815}: {
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)816 /** A phone: back to the list of conversations. */
817 onBack: () => void;
Chat and workspace agents: channels, DMs and named agents you talk to818 data: Loaded;
819 others: MemberProfile[];
820 starred: boolean;
821 muted: boolean;
822 memberCount: number;
823 live: LiveState;
824 info: boolean;
825 onStar: () => void;
826 onInfo: () => void;
827}) {
828 const channel = data.channel;
829 const agentDm = channel.kind === "dm" && others.length === 1 && others[0]!.kind === "agent" ? others[0]! : null;
830 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.831 <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">
832 <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)833 <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.834 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to835 <div className="flex min-w-0 grow items-center gap-2.5">
836 {channel.kind === "channel" ? (
837 <>
838 <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 project839 {channel.private ? (
840 <Hint label="Private channel: only its members can find and read it">
841 <span className="flex">
842 <Lock size={15} className="text-faint" aria-label="Private channel" />
843 </span>
844 </Hint>
845 ) : (
846 <Hash size={16} className="text-faint" aria-label="Public channel" />
847 )}
Chat and workspace agents: channels, DMs and named agents you talk to848 <span className="truncate">{channel.name}</span>
849 </h1>
850 <StarButton starred={starred} onClick={onStar} />
851 {muted && (
852 <Hint label="Muted: no unread count or notifications">
853 <span className="text-faint">
854 <BellOff size={14} aria-label="Muted" />
855 </span>
856 </Hint>
857 )}
Chat controls, public profiles, shadcn selects, and no Docs tab in a project858 {channel.archived_at && (
859 <Badge tone="neutral" className="shrink-0">
860 <Archive size={11} />
861 Archived
862 </Badge>
863 )}
Chat and workspace agents: channels, DMs and named agents you talk to864 {channel.topic && (
865 <>
866 <span aria-hidden="true" className="hidden h-4 w-px shrink-0 bg-line sm:block" />
867 <p className="hidden min-w-0 truncate text-sm text-muted sm:block">{channel.topic}</p>
868 </>
869 )}
870 </>
871 ) : (
872 <>
873 <span className="flex shrink-0 -space-x-1.5">
874 {others.slice(0, 3).map((member) => (
875 <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 managers876 <MemberAvatar member={member} size={26} presence={others.length === 1} />
Chat and workspace agents: channels, DMs and named agents you talk to877 </span>
878 ))}
879 </span>
880 <div className="min-w-0">
881 <h1 className="flex min-w-0 items-center gap-1.5 text-[0.9375rem] leading-tight font-semibold">
882 <span className="truncate">{others.map(shownName).join(", ") || "Just you"}</span>
883 {agentDm && <AgentPill />}
884 </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)885 {agentDm && (titleOf(agentDm) ?? agentDm.role) && (
886 <p className="truncate text-xs leading-tight text-muted">{titleOf(agentDm) ?? agentDm.role}</p>
887 )}
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers888 {/* One person: their status under their name, as their card has it. */}
889 {!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 to890 </div>
891 <StarButton starred={starred} onClick={onStar} />
892 </>
893 )}
894 </div>
895 {live !== "open" && (
896 <span role="status" className="hidden items-center gap-1.5 text-xs text-faint sm:flex">
897 <span className="size-1.5 animate-pulse rounded-full bg-warn" />
898 {live === "reconnecting" ? "Reconnecting…" : "Connecting…"}
899 </span>
900 )}
901 {channel.kind === "channel" && (
902 <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.903 <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 to904 <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.905 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to906 </Hint>
907 )}
908 {channel.kind === "dm" && (
909 <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.910 <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 to911 <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.912 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to913 </Hint>
914 )}
915 </header>
916 );
917}
918
919function UsersStack({ count }: { count: number }) {
920 return (
921 <>
922 <Users size={14} />
923 {count}
924 </>
925 );
926}
927
928function StarButton({ starred, onClick }: { starred: boolean; onClick: () => void }) {
929 return (
930 <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.931 <Button
932 variant="ghost"
933 size="icon-sm"
Chat and workspace agents: channels, DMs and named agents you talk to934 aria-label={starred ? "Unstar" : "Star"}
935 aria-pressed={starred}
936 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.937 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 to938 >
939 <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.940 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to941 </Hint>
942 );
943}
944
945/** How a conversation begins, above its first message. */
946function ConversationStart({ data, others }: { data: Loaded; others: MemberProfile[] }) {
947 const channel = data.channel;
948 if (channel.kind === "dm") {
949 const agent = others.length === 1 && others[0]!.kind === "agent" ? others[0]! : null;
950 return (
951 <div className="px-2 pb-6 sm:px-3">
952 <div className="flex -space-x-2">
953 {others.slice(0, 4).map((m) => (
954 <span key={m.id} className="rounded-full ring-4 ring-bg">
955 <MemberAvatar member={m} size={56} />
956 </span>
957 ))}
958 </div>
959 <h2 className="mt-3 flex items-center gap-2 text-xl font-semibold tracking-tight">
960 {others.map(shownName).join(", ") || "Just you"}
961 {agent && <AgentPill />}
962 </h2>
963 <p className="mt-1 max-w-xl text-sm text-muted">
964 {agent
965 ? `${agent.role ? `${agent.role}. ` : ""}Ask a question or give it a job: it answers here, and opens a task when the work needs one.`
966 : "This is the start of your conversation."}
967 </p>
968 </div>
969 );
970 }
971 return (
972 <div className="px-2 pb-6 sm:px-3">
973 <span className="flex size-12 items-center justify-center rounded-xl bg-raised text-muted">
974 {channel.private ? <Lock size={22} /> : <Hash size={24} />}
975 </span>
976 <h2 className="mt-3 text-xl font-semibold tracking-tight">Welcome to #{channel.name}</h2>
977 <p className="mt-1 max-w-xl text-sm text-muted">{channel.topic ?? "This is the very beginning of the channel."}</p>
978 </div>
979 );
980}
981
982function DayRule({ label }: { label: string }) {
983 return (
984 <div role="separator" aria-label={label} className="relative my-3 flex items-center justify-center">
985 <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.986 <Badge className="relative bg-bg px-3 py-0.5" suppressHydrationWarning>
Chat and workspace agents: channels, DMs and named agents you talk to987 {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.988 </Badge>
Chat and workspace agents: channels, DMs and named agents you talk to989 </div>
990 );
991}
992
993/** 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)994/**
995 * Someone's profile beside the conversation, opened from their card: who
996 * they are, their teams and local time, and what they said here lately.
997 * The conversation stays where it was.
998 */
999function ProfilePanel({
1000 slug,
1001 username,
1002 messages,
1003 zone,
1004 context,
1005}: {
1006 slug: string;
1007 username: string;
1008 messages: ShownMessage[];
1009 zone: string | undefined;
1010 context: TextContext;
1011}) {
1012 const [card, setCard] = useState<PersonCard | null | undefined>(undefined);
1013 useEffect(() => {
1014 let live = true;
1015 setCard(undefined);
1016 void personCard(slug, username).then((value) => {
1017 if (live) setCard(value);
1018 });
1019 return () => {
1020 live = false;
1021 };
1022 }, [slug, username]);
1023 const theirs = messages
1024 .filter((m) => m.author.kind === "user" && m.author.name === username && !m.deleted_at && m.body)
1025 .slice(-5)
1026 .reverse();
1027 const time = card ? localTime(card.timezone, Date.now()) : null;
1028 return (
1029 <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 managers1030 <WithPresence person={{ username }} size={88}>
1031 <Avatar name={username} image={card?.avatar ?? null} size={88} />
1032 </WithPresence>
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar1033 <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)1034 <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 calendar1035 @{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)1036 {card?.pronouns ? <span className="font-sans"> · {card.pronouns}</span> : null}
1037 </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 managers1038 <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)1039 {card?.bio && <p className="mt-3 text-sm leading-relaxed text-fg-soft">{card.bio}</p>}
1040 <dl className="mt-4 space-y-2 text-sm">
1041 {card && card.teams.length > 0 && (
1042 <div className="flex gap-3">
1043 <dt className="w-20 shrink-0 text-faint">Teams</dt>
1044 <dd className="text-fg-soft">{card.teams.map((team) => team.name).join(", ")}</dd>
1045 </div>
1046 )}
1047 {time && (
1048 <div className="flex gap-3">
1049 <dt className="w-20 shrink-0 text-faint">Local time</dt>
1050 <dd className="text-fg-soft" suppressHydrationWarning>
1051 {time}
1052 </dd>
1053 </div>
1054 )}
1055 {card?.location && (
1056 <div className="flex gap-3">
1057 <dt className="w-20 shrink-0 text-faint">Location</dt>
1058 <dd className="text-fg-soft">{card.location}</dd>
1059 </div>
1060 )}
1061 {card && card.workspaces.length > 0 && (
1062 <div className="flex gap-3">
1063 <dt className="w-20 shrink-0 text-faint">Workspaces</dt>
1064 <dd className="text-fg-soft">{card.workspaces.map((w) => w.name).join(", ")}</dd>
1065 </div>
1066 )}
1067 </dl>
1068 <h4 className="mt-6 mb-2 text-xs font-medium text-faint">Lately in this conversation</h4>
1069 {theirs.length === 0 ? (
1070 <p className="text-sm text-muted">Nothing here yet.</p>
1071 ) : (
1072 <ul className="space-y-3">
1073 {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.1074 <Card asChild key={m.id} radius="lg" className="px-3 py-2.5">
1075 <li>
1076 <p className="mb-1 text-[0.6875rem] text-faint" suppressHydrationWarning>
1077 {clock(m.created_at, zone)}
1078 </p>
1079 <div className="line-clamp-4 text-sm">
1080 <MessageText body={m.body} context={context} />
1081 </div>
1082 </li>
1083 </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)1084 ))}
1085 </ul>
1086 )}
1087 <Link to={`/u/${username}`} className="mt-6 inline-block text-sm text-accent hover:underline">
1088 Open their full profile
1089 </Link>
1090 </div>
1091 );
1092}
1093
1094/** An agent's title ("QA Engineer"), when the chat service sends it with the member. */
1095function titleOf(member: MemberProfile): string | null {
1096 return (member as MemberProfile & { title?: string | null }).title ?? null;
1097}
1098
1099/** A long press (half a second, without moving) on a touch screen. */
1100function useLongPress(onLongPress?: () => void) {
1101 const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
1102 const cancel = () => {
1103 if (timer.current) clearTimeout(timer.current);
1104 timer.current = null;
1105 };
1106 if (!onLongPress) return {};
1107 return {
1108 onTouchStart: () => {
1109 cancel();
1110 timer.current = setTimeout(() => {
1111 timer.current = null;
1112 if (typeof navigator !== "undefined" && "vibrate" in navigator) navigator.vibrate?.(8);
1113 onLongPress();
1114 }, 480);
1115 },
1116 onTouchMove: cancel,
1117 onTouchEnd: cancel,
1118 onTouchCancel: cancel,
1119 onContextMenu: (event: React.MouseEvent) => {
1120 // A phone's own long-press menu (copy, look up) would cover ours.
1121 if (window.matchMedia("(pointer: coarse)").matches) event.preventDefault();
1122 },
1123 };
1124}
1125
1126/** A long-pressed message's actions, in a sheet from the bottom: reply, copy, and for your own, edit and delete. */
1127function MessageActions({
1128 message,
1129 mine,
1130 onClose,
1131 onThread,
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1132 onCopyLink,
1133 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)1134 onEdit,
1135 onDelete,
1136}: {
1137 message: ShownMessage | null;
1138 mine: boolean;
1139 onClose: () => void;
1140 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 thread1141 onCopyLink: (message: ShownMessage) => void;
1142 /** Absent where the person cannot post (not joined, archived). */
1143 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)1144 onEdit: (message: ShownMessage) => void;
1145 onDelete: (message: ShownMessage) => void;
1146}) {
1147 const [confirm, setConfirm] = useState(false);
1148 useEffect(() => setConfirm(false), [message]);
1149 const act = (run: () => void) => () => {
1150 run();
1151 onClose();
1152 };
1153 return (
1154 <BottomSheet open={message != null} onOpenChange={(open) => !open && onClose()} title="Message actions">
1155 {message && (
1156 <>
1157 <p className="mt-1 mb-2 line-clamp-2 px-3 text-sm text-muted">
1158 <span className="font-medium text-fg-soft">{shownName(message.author)}:</span> {message.body || message.card?.title}
1159 </p>
1160 {!message.pending && <QuickReactions messageId={message.id} onDone={onClose} />}
1161 {!message.thread_root && (
1162 <SheetRow icon={<MessageSquareText />} onClick={act(() => onThread(message.id))}>
1163 Reply in thread
1164 </SheetRow>
1165 )}
1166 <SheetRow icon={<Copy />} onClick={act(() => void navigator.clipboard?.writeText(message.body || message.card?.title || ""))}>
1167 Copy text
1168 </SheetRow>
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1169 {!message.pending && (
1170 <SheetRow icon={<Link2 />} onClick={act(() => onCopyLink(message))}>
1171 Copy link to thread
1172 </SheetRow>
1173 )}
1174 {!message.pending && onWriteUp && (
1175 <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.1176 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 thread1177 </SheetRow>
1178 )}
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)1179 {mine && message.kind === "text" && (
1180 <SheetRow icon={<Pencil />} onClick={act(() => onEdit(message))}>
1181 Edit
1182 </SheetRow>
1183 )}
1184 {mine &&
1185 (confirm ? (
1186 <SheetRow icon={<Trash2 className="text-danger" />} onClick={act(() => onDelete(message))}>
1187 <span className="text-danger">Delete for everyone</span>
1188 </SheetRow>
1189 ) : (
1190 <SheetRow icon={<Trash2 />} onClick={() => setConfirm(true)}>
1191 Delete
1192 </SheetRow>
1193 ))}
1194 </>
1195 )}
1196 </BottomSheet>
1197 );
1198}
1199
Chat and workspace agents: channels, DMs and named agents you talk to1200function MessageRow({
1201 message,
1202 head,
1203 zone,
1204 context,
1205 code,
1206 slug,
1207 onThread,
1208 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)1209 onLongPress,
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1210 onCopyLink,
1211 onWriteUp,
Chat and workspace agents: channels, DMs and named agents you talk to1212 threadOpen,
1213 inThread,
Merge main into Artifacts Phase 21214 onEdit,
1215 editor,
Chat and workspace agents: channels, DMs and named agents you talk to1216}: {
Merge main into Artifacts Phase 21217 /** Your own message: "Edit message" in the "⋯", and the composer in its place while editing. */
1218 onEdit?: () => void;
1219 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)1220 /** A phone: a long press opens the message's actions. */
1221 onLongPress?: () => void;
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1222 /** The hover toolbar's "⋯": copy the link to its thread, write it up in Docs. */
1223 onCopyLink?: () => void;
1224 onWriteUp?: () => void;
Chat and workspace agents: channels, DMs and named agents you talk to1225 message: ShownMessage;
1226 head: boolean;
1227 zone: string | undefined;
1228 context: TextContext;
1229 code: boolean;
1230 slug: string;
1231 onThread?: () => void;
1232 onRetry: () => void;
1233 threadOpen?: boolean;
1234 inThread?: boolean;
1235}) {
1236 const author = message.author;
1237 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)1238 const press = useLongPress(onLongPress);
Chat and workspace agents: channels, DMs and named agents you talk to1239 return (
1240 <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)1241 {...press}
Chat and workspace agents: channels, DMs and named agents you talk to1242 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)1243 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 to1244 threadOpen ? "bg-accent/[0.06] hover:bg-accent/[0.08]" : ""
1245 }`}
1246 >
1247 <div className="w-9 shrink-0">
1248 {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)1249 <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 to1250 <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)1251 </MemberCard>
Chat and workspace agents: channels, DMs and named agents you talk to1252 ) : (
1253 <time
1254 dateTime={message.created_at}
1255 suppressHydrationWarning
1256 className="mt-1 block text-right text-[0.625rem] leading-5 text-faint tabular-nums opacity-0 group-hover/message:opacity-100"
1257 >
1258 {time.replace(/\s?[AP]M$/, "")}
1259 </time>
1260 )}
1261 </div>
1262 <div className="min-w-0 grow">
1263 {head && (
1264 <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)1265 <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 to1266 {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)1267 </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 managers1268 {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 to1269 {author.kind === "agent" && <AgentPill className="self-center" />}
1270 <time dateTime={message.created_at} suppressHydrationWarning className="shrink-0 text-xs text-faint tabular-nums">
1271 {time}
1272 </time>
1273 </div>
1274 )}
1275 <div className={message.pending && !message.failed ? "opacity-60" : undefined}>
Merge main into Artifacts Phase 21276 {editor ? <div className="py-1">{editor}</div> : message.body && <MessageText body={message.body} context={context} />}
1277 {!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 calendar1278 {message.card && (
1279 <CardBox
1280 card={message.card}
1281 slug={slug}
1282 code={code}
1283 channelId={message.channel_id}
1284 messageId={message.id}
1285 inert={!!message.pending || !!message.deleted_at}
1286 />
1287 )}
Chat and workspace agents: channels, DMs and named agents you talk to1288 {message.edited_at && <span className="text-[0.6875rem] text-faint"> (edited)</span>}
1289 </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)1290 {!message.pending && !message.deleted_at && <ReactionBar messageId={message.id} reactions={message.reactions} />}
Chat and workspace agents: channels, DMs and named agents you talk to1291 {message.failed && (
1292 <p className="mt-0.5 text-xs text-danger">
1293 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.1294 <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 to1295 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.1296 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to1297 </p>
1298 )}
1299 {!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.1300 <Button
Chat and workspace agents: channels, DMs and named agents you talk to1301 type="button"
1302 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.1303 variant="ghost"
1304 size="inline"
1305 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 to1306 >
1307 <MessageSquareText size={14} />
1308 {message.reply_count} {message.reply_count === 1 ? "reply" : "replies"}
1309 {message.last_reply_at && (
1310 <span className="font-normal text-faint" suppressHydrationWarning>
1311 Last reply {clock(message.last_reply_at, zone)}
1312 </span>
1313 )}
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.1314 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to1315 )}
1316 </div>
1317 {!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.1318 <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)1319 <AddReaction messageId={message.id} />
Chat and workspace agents: channels, DMs and named agents you talk to1320 <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.1321 <Button
Chat and workspace agents: channels, DMs and named agents you talk to1322 type="button"
1323 aria-label="Reply in thread"
1324 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.1325 variant="ghost"
1326 size="icon-xs"
Chat and workspace agents: channels, DMs and named agents you talk to1327 >
1328 <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.1329 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to1330 </Hint>
Merge main into Artifacts Phase 21331 {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.1332 </Card>
Chat and workspace agents: channels, DMs and named agents you talk to1333 )}
1334 </article>
1335 );
1336}
1337
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1338/** A message's "⋯" in its hover toolbar. */
Merge main into Artifacts Phase 21339function 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 thread1340 return (
1341 <DropdownMenu>
1342 <Hint label="More actions">
1343 <DropdownMenuTrigger
1344 aria-label="More actions"
1345 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"
1346 >
1347 <Ellipsis size={15} />
1348 </DropdownMenuTrigger>
1349 </Hint>
1350 <DropdownMenuContent align="end">
Merge main into Artifacts Phase 21351 {onEdit && (
1352 <DropdownMenuItem onSelect={onEdit}>
1353 <Pencil />
1354 Edit message
1355 </DropdownMenuItem>
1356 )}
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1357 <ThreadMenuItems onCopyLink={onCopyLink} onWriteUp={onWriteUp} />
1358 </DropdownMenuContent>
1359 </DropdownMenu>
1360 );
1361}
1362
1363function ThreadMenuItems({ onCopyLink, onWriteUp }: { onCopyLink: () => void; onWriteUp?: () => void }) {
1364 return (
1365 <>
1366 <DropdownMenuItem onSelect={onCopyLink}>
1367 <Link2 />
1368 Copy link to thread
1369 </DropdownMenuItem>
1370 {onWriteUp && (
1371 <DropdownMenuItem onSelect={onWriteUp}>
1372 <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.1373 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 thread1374 </DropdownMenuItem>
1375 )}
1376 </>
1377 );
1378}
1379
1380/** The thread panel's "⋯", in its header. */
1381function ThreadMenu({ onCopyLink, onWriteUp }: { onCopyLink: () => void; onWriteUp?: () => void }) {
1382 return (
1383 <DropdownMenu>
1384 <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.1385 <DropdownMenuTrigger asChild>
1386 <Button variant="ghost" size="icon-lg" aria-label="Thread actions" className="rounded-full active:bg-raised lg:size-7 lg:rounded-md">
1387 <Ellipsis size={16} />
1388 </Button>
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1389 </DropdownMenuTrigger>
1390 </Hint>
1391 <DropdownMenuContent align="end">
1392 <ThreadMenuItems onCopyLink={onCopyLink} onWriteUp={onWriteUp} />
1393 </DropdownMenuContent>
1394 </DropdownMenu>
1395 );
1396}
1397
Chat and workspace agents: channels, DMs and named agents you talk to1398/** "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)1399/** What an agent working on a reply says it is doing: one phrase per agent, so each sounds like itself. */
1400const 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"];
1401
1402function agentDoing(id: string): string {
1403 let hash = 0;
1404 for (const char of id) hash = (hash * 31 + char.charCodeAt(0)) | 0;
1405 return AGENT_DOING[Math.abs(hash) % AGENT_DOING.length]!;
1406}
1407
Chat and workspace agents: channels, DMs and named agents you talk to1408function TypingLine({ members }: { members: MemberProfile[] }) {
1409 const names = members.map(shownName);
1410 const who = names.length === 1 ? names[0] : names.length === 2 ? `${names[0]} and ${names[1]}` : names.length > 2 ? "Several people" : "";
1411 return (
1412 <div aria-live="polite" className="flex h-6 items-center gap-1.5 px-1 text-xs text-muted">
1413 {who && (
1414 <>
1415 <span aria-hidden="true" className="flex gap-0.5">
1416 {[0, 1, 2].map((i) => (
1417 <span key={i} className="size-1 animate-bounce rounded-full bg-accent" style={{ animationDelay: `${i * 140}ms` }} />
1418 ))}
1419 </span>
1420 <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)1421 <span className="font-medium text-fg-soft">{who}</span>{" "}
1422 {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 to1423 </span>
1424 </>
1425 )}
1426 </div>
1427 );
1428}
1429
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)1430/**
1431 * A panel beside the conversation: a thread, or the details. On a phone it
1432 * is a screen of its own, pushed over the conversation, as tall as what
1433 * the keyboard leaves.
1434 */
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1435function 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)1436 const swipe = useSwipeBack(onClose);
Chat and workspace agents: channels, DMs and named agents you talk to1437 return (
1438 <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)1439 {...swipe}
Chat and workspace agents: channels, DMs and named agents you talk to1440 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)1441 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 to1442 >
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)1443 <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.1444 <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)1445 <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.1446 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to1447 <h2 className="text-[0.9375rem] font-semibold">{title}</h2>
1448 {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 thread1449 <div className="ml-auto flex shrink-0 items-center gap-1">
1450 {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.1451 <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 thread1452 <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.1453 </Button>
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1454 </div>
Chat and workspace agents: channels, DMs and named agents you talk to1455 </div>
1456 <div className="flex min-h-0 grow flex-col">{children}</div>
1457 </aside>
1458 );
1459}
1460
1461function ThreadPanel({
1462 root,
1463 replies,
1464 zone,
1465 context,
1466 code,
1467 slug,
1468 people,
1469 joined,
1470 onSend,
1471 onRetry,
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1472 onTyping,
Chat and workspace agents: channels, DMs and named agents you talk to1473 draftKey,
1474}: {
1475 root: ShownMessage | null;
1476 replies: ShownMessage[] | null;
1477 zone: string | undefined;
1478 context: TextContext;
1479 code: boolean;
1480 slug: string;
1481 people: Mentionable[];
1482 joined: boolean;
1483 onSend: (body: string) => void;
1484 onRetry: (message: ShownMessage) => void;
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1485 /** Says "is typing" to the others in the channel, as the main composer does. */
1486 onTyping: () => void;
Chat and workspace agents: channels, DMs and named agents you talk to1487 draftKey: string;
1488}) {
1489 const rows = useMemo(() => timeline(replies ?? [], new Date(), zone).filter((row) => row.kind === "message"), [replies, zone]);
1490 return (
1491 <>
1492 <div className="min-h-0 grow overflow-y-auto px-1 py-3 [scrollbar-width:thin]">
1493 {root ? (
1494 <MessageRow message={root} head zone={zone} context={context} code={code} slug={slug} onRetry={() => onRetry(root)} inThread />
1495 ) : (
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1496 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 to1497 )}
1498 <div className="my-2 flex items-center gap-3 px-4 text-xs text-faint">
1499 {replies == null ? "Loading replies…" : `${replies.filter((r) => !r.deleted_at).length} ${replies.length === 1 ? "reply" : "replies"}`}
1500 <span className="h-px grow bg-line" />
1501 </div>
1502 {rows.map((row) =>
1503 row.kind === "message" ? (
1504 <MessageRow
1505 key={row.key}
1506 message={row.message}
1507 head={row.head}
1508 zone={zone}
1509 context={context}
1510 code={code}
1511 slug={slug}
1512 onRetry={() => onRetry(row.message)}
1513 inThread
1514 />
1515 ) : null,
1516 )}
1517 </div>
1518 {joined && (
1519 <div className="shrink-0 p-3">
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1520 <Composer draftKey={draftKey} placeholder="Reply…" people={people} onSend={onSend} onTyping={onTyping} compact autoFocus />
1521 </div>
1522 )}
1523 </>
1524 );
1525}
1526
1527/**
1528 * A channel's name, topic and visibility, and changing them: any member
1529 * edits the topic; renaming and archiving are for whoever the workspace's
1530 * chat settings allow (`canManage`). #general keeps its name and stays.
1531 */
1532function ChannelAbout({
1533 channel,
1534 joined,
1535 canManage,
1536 onUpdate,
1537}: {
1538 channel: Channel;
1539 joined: boolean;
1540 canManage: boolean;
1541 onUpdate: (change: ChannelChange) => Promise<Result<unknown>>;
1542}) {
1543 const [editing, setEditing] = useState<"name" | "topic" | null>(null);
1544 const [value, setValue] = useState("");
1545 const [error, setError] = useState<string | null>(null);
1546 const [busy, setBusy] = useState(false);
1547 const [confirm, setConfirm] = useState(false);
1548 const general = channel.name === "general";
1549 const archived = !!channel.archived_at;
1550 const mayRename = canManage && !general && !archived;
1551 const mayTopic = joined && !archived;
1552 const save = async (change: ChannelChange) => {
1553 setBusy(true);
1554 setError(null);
1555 const done = await onUpdate(change);
1556 setBusy(false);
1557 if (!done.ok) return setError(done.error.message);
1558 setEditing(null);
1559 setConfirm(false);
1560 };
1561 const start = (field: "name" | "topic") => {
1562 setEditing(field);
1563 setValue(field === "name" ? (channel.name ?? "") : (channel.topic ?? ""));
1564 setError(null);
1565 };
1566 const edit = (field: "name" | "topic") => (
1567 <form
1568 className="mt-1.5 space-y-2"
1569 onSubmit={(event) => {
1570 event.preventDefault();
1571 void save(field === "name" ? { name: value } : { topic: value });
1572 }}
1573 >
1574 <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">
1575 {field === "name" && (channel.private ? <Lock size={13} className="text-faint" /> : <Hash size={13} className="text-faint" />)}
1576 <input
1577 autoFocus
1578 value={value}
1579 onChange={(event) => setValue(event.target.value)}
1580 onKeyDown={(event) => event.key === "Escape" && setEditing(null)}
1581 maxLength={field === "name" ? 80 : 250}
1582 placeholder={field === "name" ? "channel-name" : "What this channel is for"}
1583 aria-label={field === "name" ? "Channel name" : "Topic"}
1584 autoComplete="off"
1585 data-1p-ignore
1586 className="min-w-0 grow bg-transparent text-sm outline-none placeholder:text-faint"
1587 />
1588 </span>
1589 <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.1590 <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 project1591 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.1592 </Button>
1593 <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 project1594 {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.1595 </Button>
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1596 </div>
1597 </form>
1598 );
1599 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.1600 <Button
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1601 type="button"
1602 onClick={() => start(field)}
1603 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.1604 variant="ghost"
1605 size="inline"
1606 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 project1607 >
1608 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.1609 </Button>
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1610 );
1611 return (
1612 <>
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.1613 <Card asChild className="group/about p-3.5">
1614 <section>
1615 <div className="flex items-center justify-between">
1616 <h3 className="text-xs font-medium text-faint">Name</h3>
1617 {mayRename && editing !== "name" && editButton("name", "Rename channel")}
1618 </div>
1619 {editing === "name" ? edit("name") : <p className="mt-1 text-sm text-fg-soft">#{channel.name}</p>}
1620 <div className="mt-3 flex items-center justify-between">
1621 <h3 className="text-xs font-medium text-faint">Topic</h3>
1622 {mayTopic && editing !== "topic" && editButton("topic", "Edit topic")}
1623 </div>
1624 {editing === "topic" ? edit("topic") : <p className="mt-1 text-sm text-fg-soft">{channel.topic || "No topic yet."}</p>}
1625 <h3 className="mt-3 text-xs font-medium text-faint">Visibility</h3>
1626 <p className="mt-1 flex items-center gap-1.5 text-sm text-fg-soft">
1627 {channel.private ? <Lock size={13} /> : <Hash size={13} />}
1628 {channel.private ? "Private: only its members can find and read it" : "Public: anyone in the workspace can read and join"}
1629 </p>
1630 {error && <p className="mt-2 text-xs text-danger">{error}</p>}
1631 </section>
1632 </Card>
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1633 {canManage && !general && (
1634 <button
1635 type="button"
1636 onClick={() => (archived ? void save({ archived: false }) : setConfirm(true))}
1637 disabled={busy}
1638 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"
1639 >
1640 {archived ? <ArchiveRestore size={14} /> : <Archive size={14} />}
1641 {archived ? "Unarchive channel" : "Archive channel"}
1642 </button>
Chat and workspace agents: channels, DMs and named agents you talk to1643 )}
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1644 <AlertDialog open={confirm} onOpenChange={setConfirm}>
1645 <AlertDialogContent>
1646 <AlertDialogHeader>
1647 <AlertDialogTitle>Archive #{channel.name}?</AlertDialogTitle>
1648 <AlertDialogDescription>
1649 It leaves everyone&apos;s sidebar and nobody can post in it. Its history stays readable, and it can be unarchived from Browse
1650 channels, Archived.
1651 </AlertDialogDescription>
1652 </AlertDialogHeader>
1653 <AlertDialogFooter>
1654 <AlertDialogCancel>Cancel</AlertDialogCancel>
1655 <AlertDialogAction
1656 onClick={(event) => {
1657 event.preventDefault();
1658 void save({ archived: true });
1659 }}
1660 disabled={busy}
1661 >
1662 {busy ? "Archiving…" : "Archive"}
1663 </AlertDialogAction>
1664 </AlertDialogFooter>
1665 </AlertDialogContent>
1666 </AlertDialog>
Chat and workspace agents: channels, DMs and named agents you talk to1667 </>
1668 );
1669}
1670
1671function InfoPanel({
1672 data,
1673 members,
1674 slug,
1675 people,
1676 onAdded,
1677 joined,
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1678 canManage,
1679 onUpdate,
Chat and workspace agents: channels, DMs and named agents you talk to1680}: {
1681 data: Loaded;
1682 members: ChannelMember[];
1683 slug: string;
1684 people: Mentionable[];
1685 onAdded: (member: ChannelMember) => void;
1686 joined: boolean;
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1687 /** Whether the viewer may rename, archive and unarchive it. */
1688 canManage: boolean;
1689 onUpdate: (change: ChannelChange) => Promise<Result<unknown>>;
Chat and workspace agents: channels, DMs and named agents you talk to1690}) {
1691 const channel = data.channel;
1692 const agents = members.filter((m) => m.member.kind === "agent");
1693 const humans = members.filter((m) => m.member.kind === "user");
1694 return (
1695 <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 project1696 {channel.kind === "channel" && <ChannelAbout channel={channel} joined={joined} canManage={canManage} onUpdate={onUpdate} />}
Chat and workspace agents: channels, DMs and named agents you talk to1697 {agents.length > 0 && (
1698 <section className="mt-5">
1699 <h3 className="mb-2 px-1 text-xs font-medium text-faint">Agents · {agents.length}</h3>
1700 <div className="flex flex-wrap gap-1.5">
1701 {agents.map(({ member }) => (
1702 <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)1703 <MemberCard
1704 member={member}
Chat and workspace agents: channels, DMs and named agents you talk to1705 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"
1706 >
1707 <MemberAvatar member={member} size={20} />
1708 {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)1709 </MemberCard>
Chat and workspace agents: channels, DMs and named agents you talk to1710 </Hint>
1711 ))}
1712 </div>
1713 </section>
1714 )}
1715 <section className="mt-5">
1716 <div className="mb-1 flex items-center justify-between px-1">
1717 <h3 className="text-xs font-medium text-faint">People · {humans.length}</h3>
1718 {joined && channel.kind === "channel" && <AddMember slug={slug} channelId={channel.id} people={people} members={members} onAdded={onAdded} />}
1719 </div>
1720 <ul className="space-y-px">
1721 {humans.map(({ member, role }) => (
1722 <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)1723 <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 managers1724 <MemberAvatar member={member} size={26} presence />
Chat and workspace agents: channels, DMs and named agents you talk to1725 <span className="min-w-0 grow truncate text-sm">
1726 {shownName(member)}
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar1727 {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 managers1728 <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 to1729 </span>
1730 {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)1731 </MemberCard>
Chat and workspace agents: channels, DMs and named agents you talk to1732 </li>
1733 ))}
1734 </ul>
1735 </section>
1736 </div>
1737 );
1738}
1739
1740/** Adding a person or an agent to the channel. An invite lets them read; it never grants write. */
1741function AddMember({
1742 slug,
1743 channelId,
1744 people,
1745 members,
1746 onAdded,
1747}: {
1748 slug: string;
1749 channelId: string;
1750 people: Mentionable[];
1751 members: ChannelMember[];
1752 onAdded: (member: ChannelMember) => void;
1753}) {
1754 const [open, setOpen] = useState(false);
1755 const [query, setQuery] = useState("");
1756 const [error, setError] = useState<string | null>(null);
1757 const send = useChatSend(slug);
1758 const chatData = useChatData();
1759 const inside = new Set(members.map((m) => `${m.member.kind}:${m.member.name.toLowerCase()}`));
1760 const q = query.trim().toLowerCase().replace(/^@/, "");
1761 const options = people
1762 .filter((p) => !inside.has(`${p.kind}:${p.name.toLowerCase()}`))
1763 .filter((p) => !q || p.name.toLowerCase().includes(q) || p.display_name.toLowerCase().includes(q))
1764 .slice(0, 8);
1765 const add = async (person: Mentionable) => {
1766 const agent = person.kind === "agent" ? chatData?.agents.find((a) => a.handle === person.name) : null;
1767 const key = agent ? `agent:${agent.id}` : `user:${person.name}`;
1768 setError(null);
1769 const done = await send({ intent: "invite", channel_id: channelId, member: key });
1770 if (!done.ok) return setError(done.error.message);
1771 onAdded({
1772 channel_id: channelId,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar1773 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 to1774 role: "member",
1775 starred: false,
1776 muted: false,
1777 last_read_id: null,
1778 joined_at: new Date().toISOString(),
1779 });
1780 setOpen(false);
1781 };
1782 return (
1783 <Popover open={open} onOpenChange={setOpen}>
1784 <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.1785 <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 to1786 <UserPlus size={13} />
1787 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.1788 </Button>
Chat and workspace agents: channels, DMs and named agents you talk to1789 </PopoverTrigger>
1790 <PopoverContent align="end" className="w-72 p-1.5">
1791 <input
1792 autoFocus
1793 value={query}
1794 onChange={(event) => setQuery(event.target.value)}
1795 placeholder="Add a person or an agent"
1796 aria-label="Add a person or an agent"
1797 autoComplete="off"
1798 data-1p-ignore
1799 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"
1800 />
1801 {options.length === 0 && <p className="px-2 py-3 text-xs text-faint">Everyone who matches is already here.</p>}
1802 {options.map((person) => (
1803 <button
1804 key={`${person.kind}:${person.name}`}
1805 type="button"
1806 onClick={() => void add(person)}
1807 className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm hover:bg-line"
1808 >
1809 <MemberAvatar member={person} size={20} />
1810 <span className="min-w-0 grow truncate">{person.display_name}</span>
1811 {person.kind === "agent" && <AgentPill />}
1812 </button>
1813 ))}
1814 {error && <p className="px-2 pt-1 text-xs text-danger">{error}</p>}
1815 <p className="border-t border-line px-2 pt-1.5 pb-0.5 text-[0.6875rem] leading-snug text-faint">
1816 Adding an agent lets it read this channel. It never grants it write access.
1817 </p>
1818 </PopoverContent>
1819 </Popover>
1820 );
1821}

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