Skip to content
582 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Merge branch 'chat-sidebar' into fast-push1import { BellOff, Bot, ChevronDown, Compass, Hash, Lock, Pin, PinOff, Plus, Search } from "lucide-react";
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)2import { ContextMenu } from "radix-ui";
Chat and workspace agents: channels, DMs and named agents you talk to3import { type ReactNode, useEffect, useState } from "react";
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)4import { Link, NavLink, useFetcher, useLocation, useNavigate } from "react-router";
Chat and workspace agents: channels, DMs and named agents you talk to5
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)6import type { ChatSidebarEntry, WorkspaceAgent } from "@g1t/contracts";
Chat and workspace agents: channels, DMs and named agents you talk to7
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)8import { conversationCache } from "./conversation-cache";
9import { CreateChannelButton, NewMessageButton, useChatData, useChatSend, useChatSidebar } from "./actions";
Chat has sounds. A short warm cue plays for a message in a conversation you have open but aren't looking at, a direct message, a mention of you and an agent finishing something for you, with a soft tick for sending that is off until you turn it on; never for your own messages, a muted conversation, a conversation you are looking at in a window that has the front, or while you have paused notifications, and never twice for one message or more than once in a second and a half. The cues are made by the browser itself in two sets, Soft and Bright, so nothing is downloaded; the Sounds and notifications section of your notification settings turns them on and off, picks the set and the volume, plays each one, and offers desktop notifications for when the window is behind; Do not disturb is the one pause everyone sees, set from the chat sidebar's bell, the settings, or your avatar menu, now with a two-hour choice, and the top bar's bell shows a dot while it's on. Your choices are kept with your account. The sound layer sits behind one small interface so the desktop app can play them natively later. The chat and notifications guides say the rules.10import { DndMenu } from "./dnd-menu";
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.11import { MemberAvatar, PairAvatar, StatusDot, statusLabel } from "./marks";
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)12import { AgentAvatar } from "../agent-avatar";
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers13import { PersonStatusEmoji } 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)14import { isOrchestrator } from "../orchestrator";
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.15import { Badge } from "../ui/badge";
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.16import { Button } from "../ui/button";
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)17import { Hint } from "../ui/hint";
Chat and workspace agents: channels, DMs and named agents you talk to18import { Skeleton } from "../ui/skeleton";
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.19import { type ChatFilter, agentDmOf, agentsAmong, channelPath, filterEntries, mixedDm, sections } from "../../lib/chat";
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)20import { feedConnected } from "../../lib/notify-client";
21import { FALLBACK_REFRESH_MS } from "../../lib/notify-store";
Chat has sounds. A short warm cue plays for a message in a conversation you have open but aren't looking at, a direct message, a mention of you and an agent finishing something for you, with a soft tick for sending that is off until you turn it on; never for your own messages, a muted conversation, a conversation you are looking at in a window that has the front, or while you have paused notifications, and never twice for one message or more than once in a second and a half. The cues are made by the browser itself in two sets, Soft and Bright, so nothing is downloaded; the Sounds and notifications section of your notification settings turns them on and off, picks the set and the volume, plays each one, and offers desktop notifications for when the window is behind; Do not disturb is the one pause everyone sees, set from the chat sidebar's bell, the settings, or your avatar menu, now with a two-hour choice, and the top bar's bell shows a dot while it's on. Your choices are kept with your account. The sound layer sits behind one small interface so the desktop app can play them natively later. The chat and notifications guides say the rules.22import { setMutedConversations } from "../../lib/sound-events";
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)23
24/** How often the sidebar asks again while the tab is shown and the feed socket is down. */
25const REFRESH_MS = FALLBACK_REFRESH_MS;
26
27const ROW =
The left-hand navigation takes less room and reads at a glance. The dock becomes a rail: a 56-pixel column of icons with the g1t mark on top, the current app marked by a filled square, hover hints naming each with its shortcut, the apps you pinned in a scrolling band, and Apps, People, Workspace and your account at the foot. Each mode's sidebar sits flush against it, 240 pixels wide, opening on Search or jump to and listing its groups under small labels in 36-pixel rows; Ctrl B hides it and the page grows into the room. Every mode's list is built on the shadcn Sidebar now in the kit, so Workspace, Code, People, Notifications, account settings, Chat, Artifacts and Agents all share one frame; the phone keeps its bottom bar and drawer. The workspaces guide describes the rail, the sidebar and the page.28 "group/row flex h-9 items-center gap-2 rounded-md pr-1.5 pl-2 text-[0.8125rem] transition-colors max-md:h-11 max-md:text-[0.9375rem] max-md:active:bg-raised";
Chat and workspace agents: channels, DMs and named agents you talk to29
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)30/** Unread on a row: lavender for a mention, quiet for the rest. */
31function Count({ entry }: { entry: ChatSidebarEntry | undefined }) {
32 if (!entry || entry.muted) return null;
33 const count = entry.mentions > 0 ? entry.mentions : entry.unread;
34 if (count <= 0) return null;
35 return (
36 <span
37 className={`min-w-5 shrink-0 rounded-full group-hover/row:invisible px-1.5 text-center text-[0.6875rem] leading-[1.125rem] font-semibold tabular-nums ${
38 entry.mentions > 0 ? "bg-accent text-bg" : "bg-line-strong text-fg"
39 }`}
40 >
41 {count}
42 </span>
43 );
44}
Chat and workspace agents: channels, DMs and named agents you talk to45
46/**
Merge branch 'chat-sidebar' into fast-push47 * Chat mode's sidebar: what you pinned, the channels (and a way to browse
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.48 * the rest), then direct messages in two buckets so it is always clear who
49 * is on the other side: People, your conversations with people, the latest
50 * first (a conversation with a person and an agent in it is here, marked
51 * as such), and Agents, your conversations with agents alone, the latest
52 * first, then the agents you have not talked to yet, g1t first: one click
53 * opens a conversation with any of them. Sections fold, and stay folded on
54 * this device. All, Unread and Mentions filter every section. It refreshes
55 * itself while the tab is shown, so unread counts move without a reload.
Chat and workspace agents: channels, DMs and named agents you talk to56 */
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)57export function ChatSidebar({ slug, heading = true }: { slug: string; heading?: boolean }) {
Chat and workspace agents: channels, DMs and named agents you talk to58 const data = useChatData();
59 const { sidebar, refresh } = useChatSidebar();
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)60 const send = useChatSend(slug);
Chat and workspace agents: channels, DMs and named agents you talk to61 const [filter, setFilter] = useState<ChatFilter>("all");
62 const [query, setQuery] = useState("");
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)63 const [opening, setOpening] = useState<string | null>(null);
Chat and workspace agents: channels, DMs and named agents you talk to64 const navigate = useNavigate();
65 const { pathname } = useLocation();
66
67 useEffect(() => {
68 if (!data) 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)69 // Notify: counts arrive live over the feed socket; this asks again only while it is down.
Chat and workspace agents: channels, DMs and named agents you talk to70 const tick = () => {
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)71 if (document.visibilityState === "visible" && !feedConnected()) refresh();
Chat and workspace agents: channels, DMs and named agents you talk to72 };
73 const timer = setInterval(tick, REFRESH_MS);
74 document.addEventListener("visibilitychange", tick);
75 return () => {
76 clearInterval(timer);
77 document.removeEventListener("visibilitychange", tick);
78 };
79 }, [data, refresh]);
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)80
81 // Pins show at once and go back if the service refuses.
82 const [pins, setPins] = useState<Map<string, boolean>>(new Map());
83 useEffect(() => setPins(new Map()), [sidebar]);
Chat has sounds. A short warm cue plays for a message in a conversation you have open but aren't looking at, a direct message, a mention of you and an agent finishing something for you, with a soft tick for sending that is off until you turn it on; never for your own messages, a muted conversation, a conversation you are looking at in a window that has the front, or while you have paused notifications, and never twice for one message or more than once in a second and a half. The cues are made by the browser itself in two sets, Soft and Bright, so nothing is downloaded; the Sounds and notifications section of your notification settings turns them on and off, picks the set and the volume, plays each one, and offers desktop notifications for when the window is behind; Do not disturb is the one pause everyone sees, set from the chat sidebar's bell, the settings, or your avatar menu, now with a two-hour choice, and the top bar's bell shows a dot while it's on. Your choices are kept with your account. The sound layer sits behind one small interface so the desktop app can play them natively later. The chat and notifications guides say the rules.84 // Sounds leave muted conversations alone (lib/sound-events.ts).
85 useEffect(() => {
86 if (sidebar) setMutedConversations(sidebar.entries.filter((e) => e.muted).map((e) => e.channel.id));
87 }, [sidebar]);
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)88 const entries = (sidebar?.entries ?? []).map((e) => (pins.has(e.channel.id) ? { ...e, starred: pins.get(e.channel.id)! } : e));
Chat and workspace agents: channels, DMs and named agents you talk to89 const shown = filterEntries(entries, filter, query);
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.90 const { pinned, channels, people, agents: agentConversations } = sections(shown);
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)91 const { agentDms } = sections(entries);
Chat and workspace agents: channels, DMs and named agents you talk to92 const loading = !data;
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)93 const owner = data?.role === "owner";
94
Merge branch 'chat-sidebar' into fast-push95 // The agents you have not talked to yet, g1t first: one click opens a
96 // conversation, which then lists under Direct messages like anyone's.
97 // Nothing of theirs is unread, so Unread and Mentions leave them out.
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)98 const q = query.trim().toLowerCase().replace(/^@/, "");
Merge branch 'chat-sidebar' into fast-push99 const agents =
100 filter !== "all"
101 ? []
102 : [...(data?.agents ?? [])]
103 .filter((agent) => !agentDms.has(agent.id))
104 .sort((a, b) => Number(isOrchestrator(b)) - Number(isOrchestrator(a)) || a.display_name.localeCompare(b.display_name))
105 .filter((agent) => !q || agent.handle.includes(q) || agent.display_name.toLowerCase().includes(q) || agent.role.toLowerCase().includes(q));
106
107 // Folded sections, remembered per workspace on this device. A folded
108 // section still shows what is unread and the conversation open now.
109 const [folded, setFolded] = useState<ReadonlySet<string>>(new Set());
110 useEffect(() => setFolded(readFolded(slug)), [slug]);
111 const fold = (id: string) =>
112 setFolded((now) => {
113 const next = new Set(now);
114 if (!next.delete(id)) next.add(id);
115 writeFolded(slug, next);
116 return next;
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)117 });
Merge branch 'chat-sidebar' into fast-push118 const keepShown = (entry: ChatSidebarEntry) => {
119 const to = channelPath(slug, entry.channel);
120 return pathname === to || pathname.startsWith(`${to}/`) || (!entry.muted && (entry.unread > 0 || entry.mentions > 0));
121 };
Chat and workspace agents: channels, DMs and named agents you talk to122
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)123 /** The direct message with an agent: opened, or made on first use. */
124 const openAgent = async (agent: WorkspaceAgent, pin = false) => {
125 const had = agentDms.get(agent.id);
126 if (had && !pin) return navigate(channelPath(slug, had.channel));
127 setOpening(agent.id);
128 const opened = had ? { ok: true as const, value: { channel: had.channel, to: channelPath(slug, had.channel) } } : await send<{ channel: ChatSidebarEntry["channel"]; to: string }>({ intent: "dm", members: [`agent:${agent.id}`] });
129 if (opened.ok && pin) await send({ intent: "preferences", channel_id: opened.value.channel.id, starred: !had?.starred });
130 setOpening(null);
131 if (opened.ok && !pin) navigate(opened.value.to);
132 if (pin) refresh();
133 };
134 const togglePin = async (entry: ChatSidebarEntry) => {
135 const next = !entry.starred;
136 setPins((now) => new Map(now).set(entry.channel.id, next));
137 const done = await send({ intent: "preferences", channel_id: entry.channel.id, starred: next });
138 if (!done.ok)
139 setPins((now) => {
140 const back = new Map(now);
141 back.delete(entry.channel.id);
142 return back;
143 });
144 };
145
146 const conversation = (entry: ChatSidebarEntry) => (
147 <ConversationRow key={entry.channel.id} entry={entry} slug={slug} pathname={pathname} agents={data?.agents ?? []} onPin={() => void togglePin(entry)} />
148 );
149
Chat and workspace agents: channels, DMs and named agents you talk to150 return (
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers151 // The colour behind it, for the dots cut out of people's avatars.
The left-hand navigation takes less room and reads at a glance. The dock becomes a rail: a 56-pixel column of icons with the g1t mark on top, the current app marked by a filled square, hover hints naming each with its shortcut, the apps you pinned in a scrolling band, and Apps, People, Workspace and your account at the foot. Each mode's sidebar sits flush against it, 240 pixels wide, opening on Search or jump to and listing its groups under small labels in 36-pixel rows; Ctrl B hides it and the page grows into the room. Every mode's list is built on the shadcn Sidebar now in the kit, so Workspace, Code, People, Notifications, account settings, Chat, Artifacts and Agents all share one frame; the phone keeps its bottom bar and drawer. The workspaces guide describes the rail, the sidebar and the page.152 <div className="flex h-full flex-col [--chat-sidebar-bg:var(--color-sidebar)]">
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)153 {heading && (
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.154 <div className="flex h-9 shrink-0 items-center justify-between pr-1 pl-3">
The left-hand navigation takes less room and reads at a glance. The dock becomes a rail: a 56-pixel column of icons with the g1t mark on top, the current app marked by a filled square, hover hints naming each with its shortcut, the apps you pinned in a scrolling band, and Apps, People, Workspace and your account at the foot. Each mode's sidebar sits flush against it, 240 pixels wide, opening on Search or jump to and listing its groups under small labels in 36-pixel rows; Ctrl B hides it and the page grows into the room. Every mode's list is built on the shadcn Sidebar now in the kit, so Workspace, Code, People, Notifications, account settings, Chat, Artifacts and Agents all share one frame; the phone keeps its bottom bar and drawer. The workspaces guide describes the rail, the sidebar and the page.155 <h2 className="text-[0.6875rem] font-medium tracking-wide text-faint uppercase">Chat</h2>
Chat has sounds. A short warm cue plays for a message in a conversation you have open but aren't looking at, a direct message, a mention of you and an agent finishing something for you, with a soft tick for sending that is off until you turn it on; never for your own messages, a muted conversation, a conversation you are looking at in a window that has the front, or while you have paused notifications, and never twice for one message or more than once in a second and a half. The cues are made by the browser itself in two sets, Soft and Bright, so nothing is downloaded; the Sounds and notifications section of your notification settings turns them on and off, picks the set and the volume, plays each one, and offers desktop notifications for when the window is behind; Do not disturb is the one pause everyone sees, set from the chat sidebar's bell, the settings, or your avatar menu, now with a two-hour choice, and the top bar's bell shows a dot while it's on. Your choices are kept with your account. The sound layer sits behind one small interface so the desktop app can play them natively later. The chat and notifications guides say the rules.156 <div className="flex items-center gap-0.5">
157 <DndMenu />
158 <NewMessageButton slug={slug} />
159 </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)160 </div>
161 )}
Chat and workspace agents: channels, DMs and named agents you talk to162 <div className="space-y-2.5 px-2.5 pt-3">
Chat has sounds. A short warm cue plays for a message in a conversation you have open but aren't looking at, a direct message, a mention of you and an agent finishing something for you, with a soft tick for sending that is off until you turn it on; never for your own messages, a muted conversation, a conversation you are looking at in a window that has the front, or while you have paused notifications, and never twice for one message or more than once in a second and a half. The cues are made by the browser itself in two sets, Soft and Bright, so nothing is downloaded; the Sounds and notifications section of your notification settings turns them on and off, picks the set and the volume, plays each one, and offers desktop notifications for when the window is behind; Do not disturb is the one pause everyone sees, set from the chat sidebar's bell, the settings, or your avatar menu, now with a two-hour choice, and the top bar's bell shows a dot while it's on. Your choices are kept with your account. The sound layer sits behind one small interface so the desktop app can play them natively later. The chat and notifications guides say the rules.163 <div className="flex items-center gap-2">
164 <label className="flex h-8 min-w-0 grow items-center gap-2 rounded-md bg-surface px-2.5 text-[0.8125rem] ring-1 ring-line transition-shadow focus-within:ring-accent-dim/70">
165 <Search size={14} className="shrink-0 text-faint" />
166 <input
167 value={query}
168 onChange={(event) => setQuery(event.target.value)}
169 onKeyDown={(event) => {
170 if (event.key === "Enter") {
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.171 const first = [...pinned, ...channels, ...people, ...agentConversations][0];
Chat has sounds. A short warm cue plays for a message in a conversation you have open but aren't looking at, a direct message, a mention of you and an agent finishing something for you, with a soft tick for sending that is off until you turn it on; never for your own messages, a muted conversation, a conversation you are looking at in a window that has the front, or while you have paused notifications, and never twice for one message or more than once in a second and a half. The cues are made by the browser itself in two sets, Soft and Bright, so nothing is downloaded; the Sounds and notifications section of your notification settings turns them on and off, picks the set and the volume, plays each one, and offers desktop notifications for when the window is behind; Do not disturb is the one pause everyone sees, set from the chat sidebar's bell, the settings, or your avatar menu, now with a two-hour choice, and the top bar's bell shows a dot while it's on. Your choices are kept with your account. The sound layer sits behind one small interface so the desktop app can play them natively later. The chat and notifications guides say the rules.172 if (first) {
173 navigate(channelPath(slug, first.channel));
174 setQuery("");
175 } else if (agents[0]) void openAgent(agents[0]);
176 } else if (event.key === "Escape") setQuery("");
177 }}
178 placeholder="Jump to channel or person"
179 aria-label="Jump to channel or person"
180 autoComplete="off"
181 data-1p-ignore
182 className="min-w-0 grow bg-transparent text-fg outline-none placeholder:text-faint"
183 />
184 </label>
185 {/* A phone has no heading row: Do not disturb sits by the search box instead. */}
186 {!heading && <DndMenu className="size-8 max-md:size-11" />}
187 </div>
Chat and workspace agents: channels, DMs and named agents you talk to188 <div role="radiogroup" aria-label="Show" className="grid grid-cols-3 rounded-md bg-surface p-0.5 ring-1 ring-line">
189 {(["all", "unread", "mentions"] as const).map((key) => (
190 <button
191 key={key}
192 type="button"
193 role="radio"
194 aria-checked={filter === key}
195 onClick={() => setFilter(key)}
196 className={`h-6 rounded-[5px] text-xs font-medium capitalize transition-colors ${
197 filter === key ? "bg-raised text-fg shadow-sm ring-1 ring-line-strong" : "text-muted hover:text-fg"
198 }`}
199 >
200 {key}
201 </button>
202 ))}
203 </div>
204 </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)205 {/* A phone's list (no heading): room at the end so the compose button never covers the last row. */}
206 <nav
207 aria-label="Conversations"
208 className={`min-h-0 grow overflow-y-auto px-2.5 pt-3 [scrollbar-width:thin] ${heading ? "pb-4" : "pb-[calc(5.5rem+env(safe-area-inset-bottom))]"}`}
209 >
Chat and workspace agents: channels, DMs and named agents you talk to210 {loading ? (
211 <SidebarSkeleton />
212 ) : !sidebar ? (
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)213 <p className="px-2 py-3 text-xs leading-relaxed text-faint">Chat didn&apos;t answer. Your conversations will show here once it does.</p>
Chat and workspace agents: channels, DMs and named agents you talk to214 ) : (
215 <>
Merge branch 'chat-sidebar' into fast-push216 {pinned.length > 0 && (
217 <Section id="pinned" title="Pinned" folded={folded} onFold={fold} keep={pinned.filter(keepShown).map(conversation)}>
218 {pinned.map(conversation)}
219 </Section>
220 )}
221 <Section
222 id="channels"
223 title="Channels"
224 folded={folded}
225 onFold={fold}
226 action={<CreateChannelButton slug={slug} />}
227 keep={channels.filter(keepShown).map(conversation)}
228 >
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)229 {channels.map(conversation)}
Chat and workspace agents: channels, DMs and named agents you talk to230 {channels.length === 0 && filter === "all" && !query && <Empty>No channels yet.</Empty>}
Merge branch 'chat-sidebar' into fast-push231 {filter === "all" && !query && (
232 <ExtraRow to={`/${slug}/-/chat/browse`} icon={<Compass size={15} />} count={sidebar.browsable > 0 ? sidebar.browsable : undefined} countLabel="to join">
233 Browse channels
234 </ExtraRow>
235 )}
Chat and workspace agents: channels, DMs and named agents you talk to236 </Section>
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)237 <Section
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.238 id="people"
239 title="People"
Merge branch 'chat-sidebar' into fast-push240 folded={folded}
241 onFold={fold}
242 action={<NewMessageButton slug={slug} />}
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.243 keep={people.filter(keepShown).map(conversation)}
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)244 >
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.245 {people.map(conversation)}
246 {people.length === 0 && filter === "all" && !query && <Empty>Message a teammate.</Empty>}
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)247 </Section>
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.248 {(agentConversations.length > 0 || agents.length > 0 || (filter === "all" && !query)) && (
Merge branch 'chat-sidebar' into fast-push249 <Section
250 id="agents"
251 title="Agents"
252 folded={folded}
253 onFold={fold}
254 action={
255 owner ? (
256 <Hint label="New agent">
257 <Link
258 to={`/${slug}/-/agents/new`}
259 aria-label="New agent"
260 className="flex size-7 items-center justify-center rounded-md text-faint transition-colors hover:bg-raised hover:text-fg"
261 >
262 <Plus size={14} />
263 </Link>
264 </Hint>
265 ) : null
266 }
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.267 keep={agentConversations.filter(keepShown).map(conversation)}
Merge branch 'chat-sidebar' into fast-push268 >
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.269 {agentConversations.map(conversation)}
270 {agentConversations.length === 0 && agents.length === 0 && filter === "all" && !query && <Empty>Message an agent.</Empty>}
Merge branch 'chat-sidebar' into fast-push271 {agents.map((agent) => (
272 <AgentRow
273 key={agent.id}
274 agent={agent}
275 busy={opening === agent.id}
276 onOpen={() => void openAgent(agent)}
277 onPin={() => void openAgent(agent, true)}
278 />
279 ))}
280 {filter === "all" && !query && (
281 <ExtraRow to={`/${slug}/-/agents`} icon={<Bot size={15} />}>
282 {(data?.agents ?? []).length === 0 ? (owner ? "Hire an agent" : "No agents yet") : "All agents"}
283 </ExtraRow>
284 )}
285 </Section>
286 )}
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)287 {shown.length === 0 && agents.length === 0 && (filter !== "all" || query) && (
288 <p className="px-2 py-2 text-xs text-faint">{query ? "Nothing matches." : filter === "unread" ? "You're all caught up." : "No mentions."}</p>
Chat and workspace agents: channels, DMs and named agents you talk to289 )}
290 </>
291 )}
292 </nav>
293 </div>
294 );
295}
296
Merge branch 'chat-sidebar' into fast-push297const FOLDED_KEY = (slug: string) => `g1t:chat-folded:${slug}`;
298
299/** The sections folded in this workspace, as last left; none if storage is not there. */
300function readFolded(slug: string): ReadonlySet<string> {
301 try {
302 const saved = JSON.parse(localStorage.getItem(FOLDED_KEY(slug)) ?? "[]");
303 return new Set(Array.isArray(saved) ? saved.filter((id): id is string => typeof id === "string") : []);
304 } catch {
305 return new Set();
306 }
307}
308
309function writeFolded(slug: string, folded: ReadonlySet<string>) {
310 try {
311 if (folded.size === 0) localStorage.removeItem(FOLDED_KEY(slug));
312 else localStorage.setItem(FOLDED_KEY(slug), JSON.stringify([...folded]));
313 } catch {
314 // Private windows and blocked storage: folding still works until a reload.
315 }
316}
317
318/**
319 * A titled part of the sidebar that folds away. Folded, it keeps `keep`
320 * showing: the unread conversations and the open one.
321 */
322function Section({
323 id,
324 title,
325 folded,
326 onFold,
327 action,
328 keep,
329 children,
330}: {
331 id: string;
332 title: string;
333 folded: ReadonlySet<string>;
334 onFold: (id: string) => void;
335 action?: ReactNode;
336 keep?: ReactNode[];
337 children: ReactNode;
338}) {
339 const open = !folded.has(id);
340 const list = `chat-section-${id}`;
Chat and workspace agents: channels, DMs and named agents you talk to341 return (
342 <section className="mb-3">
343 <div className="group/head flex h-7 items-center justify-between pr-0.5 pl-1">
344 <button
345 type="button"
Merge branch 'chat-sidebar' into fast-push346 onClick={() => onFold(id)}
Chat and workspace agents: channels, DMs and named agents you talk to347 aria-expanded={open}
Merge branch 'chat-sidebar' into fast-push348 aria-controls={list}
The left-hand navigation takes less room and reads at a glance. The dock becomes a rail: a 56-pixel column of icons with the g1t mark on top, the current app marked by a filled square, hover hints naming each with its shortcut, the apps you pinned in a scrolling band, and Apps, People, Workspace and your account at the foot. Each mode's sidebar sits flush against it, 240 pixels wide, opening on Search or jump to and listing its groups under small labels in 36-pixel rows; Ctrl B hides it and the page grows into the room. Every mode's list is built on the shadcn Sidebar now in the kit, so Workspace, Code, People, Notifications, account settings, Chat, Artifacts and Agents all share one frame; the phone keeps its bottom bar and drawer. The workspaces guide describes the rail, the sidebar and the page.349 className="flex min-w-0 grow items-center gap-1 rounded px-1 py-1 text-left text-[0.6875rem] font-medium tracking-wide text-faint uppercase transition-colors hover:text-muted focus-visible:ring-2 focus-visible:ring-accent focus-visible:outline-none max-md:text-[0.8125rem]"
Chat and workspace agents: channels, DMs and named agents you talk to350 >
Merge branch 'chat-sidebar' into fast-push351 <ChevronDown size={12} className={`shrink-0 transition-transform ${open ? "" : "-rotate-90"}`} />
Chat and workspace agents: channels, DMs and named agents you talk to352 {title}
353 </button>
Merge branch 'chat-sidebar' into fast-push354 <span className="opacity-70 transition-opacity group-hover/head:opacity-100 focus-within:opacity-100 [@media(hover:none)]:opacity-100">{action}</span>
Chat and workspace agents: channels, DMs and named agents you talk to355 </div>
Merge branch 'chat-sidebar' into fast-push356 {(open || (keep && keep.length > 0)) && (
357 <ul id={list} className="space-y-px">
358 {open ? children : keep}
359 </ul>
360 )}
Chat and workspace agents: channels, DMs and named agents you talk to361 </section>
362 );
363}
364
Merge branch 'chat-sidebar' into fast-push365/** A quieter row at the end of a section: somewhere to go rather than a conversation. */
366function ExtraRow({ to, icon, count, countLabel, children }: { to: string; icon: ReactNode; count?: number; countLabel?: string; children: ReactNode }) {
367 return (
368 <li>
369 <NavLink
370 to={to}
371 end
The left-hand navigation takes less room and reads at a glance. The dock becomes a rail: a 56-pixel column of icons with the g1t mark on top, the current app marked by a filled square, hover hints naming each with its shortcut, the apps you pinned in a scrolling band, and Apps, People, Workspace and your account at the foot. Each mode's sidebar sits flush against it, 240 pixels wide, opening on Search or jump to and listing its groups under small labels in 36-pixel rows; Ctrl B hides it and the page grows into the room. Every mode's list is built on the shadcn Sidebar now in the kit, so Workspace, Code, People, Notifications, account settings, Chat, Artifacts and Agents all share one frame; the phone keeps its bottom bar and drawer. The workspaces guide describes the rail, the sidebar and the page.372 className={({ isActive }) => `${ROW} ${isActive ? "bg-sidebar-accent text-fg" : "text-faint hover:bg-sidebar-accent/60 hover:text-muted"}`}
Merge branch 'chat-sidebar' into fast-push373 >
374 <span className="flex w-4.5 shrink-0 justify-center">{icon}</span>
375 <span className="min-w-0 grow truncate">{children}</span>
376 {count != null && (
377 <span className="shrink-0 text-xs tabular-nums">
378 {count}
379 {countLabel && <span className="sr-only"> {countLabel}</span>}
380 </span>
381 )}
382 </NavLink>
383 </li>
384 );
385}
386
Chat and workspace agents: channels, DMs and named agents you talk to387function Empty({ children }: { children: ReactNode }) {
388 return <li className="px-2 py-1 text-xs text-faint">{children}</li>;
389}
390
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)391/** Pin or unpin, on hover or focus; the same as the context menu's first item. */
392function PinButton({ pinned, onPin, label }: { pinned: boolean; onPin: () => void; label: string }) {
393 return (
394 <Hint label={pinned ? "Unpin" : "Pin to the top"}>
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.395 <Button
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)396 type="button"
397 aria-label={`${pinned ? "Unpin" : "Pin"} ${label}`}
398 onClick={(event) => {
399 event.preventDefault();
400 event.stopPropagation();
401 onPin();
402 }}
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.403 variant="secondary" size="icon"
404 className="absolute top-1/2 right-1 hidden size-6 -translate-y-1/2 rounded text-faint group-hover/row:flex group-focus-within/row:flex hover:text-fg [@media(hover:none)]: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)405 >
406 {pinned ? <PinOff size={13} /> : <Pin size={13} />}
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.407 </Button>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)408 </Hint>
409 );
410}
411
412/** Right-click on a row: pin it, or open it. */
413function RowMenu({ pinned, onPin, to, children }: { pinned: boolean; onPin: () => void; to?: string; children: ReactNode }) {
414 const navigate = useNavigate();
415 const item =
416 "flex cursor-default items-center gap-2 rounded-md px-2 py-1.5 text-[0.8125rem] text-fg/90 outline-none select-none data-highlighted:bg-line data-highlighted:text-fg [&_svg]:size-4 [&_svg]:text-muted";
417 return (
418 <ContextMenu.Root>
419 <ContextMenu.Trigger asChild>{children}</ContextMenu.Trigger>
420 <ContextMenu.Portal>
421 <ContextMenu.Content className="z-50 min-w-44 rounded-lg border border-line-strong bg-raised p-1 shadow-xl shadow-black/40 data-[state=open]:animate-pop-in">
422 <ContextMenu.Item className={item} onSelect={onPin}>
423 {pinned ? <PinOff /> : <Pin />}
424 {pinned ? "Unpin" : "Pin to the top"}
425 </ContextMenu.Item>
426 {to && (
427 <ContextMenu.Item className={item} onSelect={() => navigate(to)}>
428 <Hash />
429 Open
430 </ContextMenu.Item>
431 )}
432 </ContextMenu.Content>
433 </ContextMenu.Portal>
434 </ContextMenu.Root>
435 );
436}
437
438/**
439 * Reads a conversation ahead, on hover, focus or the start of a touch, so
440 * opening it draws at once (its route's `clientLoader` keeps it).
441 */
442function usePrefetchConversation(to: string) {
443 const fetcher = useFetcher();
444 const load = () => {
445 if (fetcher.state === "idle" && fetcher.data === undefined && !conversationCache.has(to)) fetcher.load(to);
446 };
447 return { onPointerEnter: load, onFocus: load, onTouchStart: load };
448}
449
450/** One channel or direct message: its mark, its name, its unread count, and a pin. */
451function ConversationRow({
452 entry,
453 slug,
454 pathname,
455 agents,
456 onPin,
457}: {
458 entry: ChatSidebarEntry;
459 slug: string;
460 pathname: string;
461 agents: WorkspaceAgent[];
462 onPin: () => void;
463}) {
Chat and workspace agents: channels, DMs and named agents you talk to464 const to = channelPath(slug, entry.channel);
465 const current = pathname === to || pathname.startsWith(`${to}/`);
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)466 const prefetch = usePrefetchConversation(to);
Chat and workspace agents: channels, DMs and named agents you talk to467 const unread = !entry.muted && (entry.unread > 0 || entry.mentions > 0);
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)468 const agentId = agentDmOf(entry);
469 const agent = agentId ? agents.find((a) => a.id === agentId) : null;
Chat and workspace agents: channels, DMs and named agents you talk to470 const other = entry.others[0];
Merge branch 'chat-sidebar' into fast-push471 const g1t = agent ? isOrchestrator(agent) : false;
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.472 // A person and an agent together: both faces, the agent's marked, and the word.
473 const mixed = mixedDm(entry);
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)474 const icon = agent ? (
Merge branch 'chat-sidebar' into fast-push475 <AgentFace agent={agent} g1t={g1t} />
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.476 ) : mixed ? (
477 <PairAvatar person={entry.others.find((m) => m.kind === "user")!} agent={agentsAmong(entry.others)[0]!} size={20} ring="var(--chat-sidebar-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)478 ) : entry.channel.kind === "dm" && other ? (
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers479 // One person: their dot, cut out of the sidebar behind it.
480 <MemberAvatar member={other} size={18} presence={entry.others.length === 1} ring="var(--chat-sidebar-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)481 ) : entry.channel.private ? (
482 <Lock size={14} />
483 ) : (
484 <Hash size={15} />
485 );
486 return (
487 <li className="group/row relative">
488 <RowMenu pinned={entry.starred} onPin={onPin} to={to}>
489 <NavLink
490 to={to}
491 {...prefetch}
492 aria-current={current ? "page" : undefined}
493 className={`${ROW} ${
The left-hand navigation takes less room and reads at a glance. The dock becomes a rail: a 56-pixel column of icons with the g1t mark on top, the current app marked by a filled square, hover hints naming each with its shortcut, the apps you pinned in a scrolling band, and Apps, People, Workspace and your account at the foot. Each mode's sidebar sits flush against it, 240 pixels wide, opening on Search or jump to and listing its groups under small labels in 36-pixel rows; Ctrl B hides it and the page grows into the room. Every mode's list is built on the shadcn Sidebar now in the kit, so Workspace, Code, People, Notifications, account settings, Chat, Artifacts and Agents all share one frame; the phone keeps its bottom bar and drawer. The workspaces guide describes the rail, the sidebar and the page.494 current ? "bg-sidebar-accent text-fg" : unread ? "text-fg hover:bg-sidebar-accent/60" : entry.muted ? "text-faint hover:bg-sidebar-accent/60 hover:text-muted" : "text-muted hover:bg-sidebar-accent/60 hover:text-fg"
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)495 }`}
496 >
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.497 <span className={`flex shrink-0 justify-center ${mixed ? "w-7" : "w-4.5"} ${current || unread ? "text-muted" : "text-faint"}`}>{icon}</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 managers498 <span className={`min-w-0 grow truncate ${unread ? "font-semibold" : current ? "font-medium" : ""}`}>
Merge branch 'chat-sidebar' into fast-push499 {agent ? agent.display_name : entry.title}
500 {agent && <span className="ml-1.5 text-[0.6875rem] font-normal text-faint">{g1t ? "orchestrator" : agent.title}</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 managers501 {entry.channel.kind === "dm" && other?.kind === "user" && entry.others.length === 1 && (
502 <PersonStatusEmoji person={{ id: other.id, username: other.name }} size={13} className="ml-1.5 align-[-2px]" inert />
503 )}
504 </span>
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.505 {mixed && (
506 <Badge tone="accent" className="shrink-0 px-1.5 font-normal">
507 with an agent
508 </Badge>
509 )}
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)510 {entry.muted && <BellOff size={12} className="shrink-0 text-faint" aria-label="Muted" />}
511 <Count entry={entry} />
512 </NavLink>
513 </RowMenu>
514 <PinButton pinned={entry.starred} onPin={onPin} label={entry.title} />
515 </li>
516 );
517}
518
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.519/**
520 * An agent's face in a row, the agent marker ringed in the sidebar's own
521 * colour; any but g1t carries its status in the other corner.
522 */
Merge branch 'chat-sidebar' into fast-push523function AgentFace({ agent, g1t }: { agent: WorkspaceAgent; g1t: boolean }) {
524 return (
525 <span className="relative inline-flex shrink-0">
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.526 <AgentAvatar agent={{ ...agent, builtin: g1t }} size={18} ring="var(--chat-sidebar-bg)" />
Merge branch 'chat-sidebar' into fast-push527 {!g1t && agent.status !== "idle" && (
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.528 <span className="absolute -top-1 -right-1 flex rounded-full p-[2px]" style={{ background: "var(--chat-sidebar-bg)" }}>
Merge branch 'chat-sidebar' into fast-push529 <StatusDot status={agent.status} className="size-[7px] shadow-none" />
530 </span>
531 )}
532 </span>
533 );
534}
535
536/** An agent you have not talked to yet: its face, name and title. Opening it starts your conversation. */
537function AgentRow({ agent, busy, onOpen, onPin }: { agent: WorkspaceAgent; busy: boolean; onOpen: () => void; onPin: () => 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)538 const g1t = isOrchestrator(agent);
Chat and workspace agents: channels, DMs and named agents you talk to539 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)540 <li className="group/row relative">
Merge branch 'chat-sidebar' into fast-push541 <RowMenu pinned={false} onPin={onPin}>
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)542 <button
543 type="button"
544 onClick={onOpen}
545 aria-busy={busy || undefined}
Merge branch 'chat-sidebar' into fast-push546 aria-label={`Message ${agent.display_name}, ${g1t ? "orchestrator" : agent.title}${g1t ? "" : `, ${statusLabel(agent.status).toLowerCase()}`}`}
The left-hand navigation takes less room and reads at a glance. The dock becomes a rail: a 56-pixel column of icons with the g1t mark on top, the current app marked by a filled square, hover hints naming each with its shortcut, the apps you pinned in a scrolling band, and Apps, People, Workspace and your account at the foot. Each mode's sidebar sits flush against it, 240 pixels wide, opening on Search or jump to and listing its groups under small labels in 36-pixel rows; Ctrl B hides it and the page grows into the room. Every mode's list is built on the shadcn Sidebar now in the kit, so Workspace, Code, People, Notifications, account settings, Chat, Artifacts and Agents all share one frame; the phone keeps its bottom bar and drawer. The workspaces guide describes the rail, the sidebar and the page.547 className={`${ROW} w-full text-left text-muted hover:bg-sidebar-accent/60 hover:text-fg ${busy ? "opacity-60" : ""}`}
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)548 >
Merge branch 'chat-sidebar' into fast-push549 <span className="flex w-4.5 shrink-0 justify-center">
550 <AgentFace agent={agent} g1t={g1t} />
Chat and workspace agents: channels, DMs and named agents you talk to551 </span>
Merge branch 'chat-sidebar' into fast-push552 <span className="min-w-0 grow truncate">
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)553 {agent.display_name}
Merge branch 'chat-sidebar' into fast-push554 <span className="ml-1.5 text-[0.6875rem] text-faint">{g1t ? "orchestrator" : agent.title}</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)555 </span>
556 </button>
557 </RowMenu>
Merge branch 'chat-sidebar' into fast-push558 <PinButton pinned={false} onPin={onPin} label={agent.display_name} />
Chat and workspace agents: channels, DMs and named agents you talk to559 </li>
560 );
561}
562
563function SidebarSkeleton() {
564 return (
565 <div className="space-y-2 px-2 pt-1" aria-busy="true">
566 <Skeleton className="h-3 w-16" />
567 {[70, 52, 84, 60].map((w) => (
568 <div key={w} className="flex h-6 items-center gap-2">
569 <Skeleton className="size-4 rounded" />
570 <Skeleton className="h-3" style={{ width: `${w}%` }} />
571 </div>
572 ))}
573 <Skeleton className="mt-4 h-3 w-24" />
574 {[64, 48, 72].map((w) => (
575 <div key={w} className="flex h-6 items-center gap-2">
576 <Skeleton className="size-4 rounded-full" />
577 <Skeleton className="h-3" style={{ width: `${w}%` }} />
578 </div>
579 ))}
580 </div>
581 );
582}

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