Skip to content
583 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";
An archived agent can be found, brought back, or deleted for good. The Agents page ends with Archived, folded, where owners see every archived agent and members their own personal ones, each with when and by whom it was archived and what deleting it would take; Restore brings it back, under a new handle when a live agent has taken its old one, and Delete permanently asks for the handle to be typed and then removes its sessions, replies, memories, routines, versions, drafts, skills and its computer's home, leaving its messages in chat under its saved name, the workspace's spend totals and the audit log. In Chat an archived agent is deactivated the way people expect: never offered as someone to message or mention, never woken, a conversation with it kept in the list, dimmed and marked Archived, open to read with a note in place of the composer and a way to restore it for owners; after deletion the conversation still reads, its messages marked as a deleted agent's. The agents, chat and agent budgets guides say so.19import { type ChatFilter, agentDmOf, agentsAmong, agentsToTalkTo, archivedAgentsDm, 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.
An archived agent can be found, brought back, or deleted for good. The Agents page ends with Archived, folded, where owners see every archived agent and members their own personal ones, each with when and by whom it was archived and what deleting it would take; Restore brings it back, under a new handle when a live agent has taken its old one, and Delete permanently asks for the handle to be typed and then removes its sessions, replies, memories, routines, versions, drafts, skills and its computer's home, leaving its messages in chat under its saved name, the workspace's spend totals and the audit log. In Chat an archived agent is deactivated the way people expect: never offered as someone to message or mention, never woken, a conversation with it kept in the list, dimmed and marked Archived, open to read with a note in place of the composer and a way to restore it for owners; after deletion the conversation still reads, its messages marked as a deleted agent's. The agents, chat and agent budgets guides say so.98 // An archived agent is never offered (lib/chat.ts `agentsToTalkTo`).
99 const agents = filter !== "all" ? [] : agentsToTalkTo(data?.agents ?? [], agentDms, query);
Merge branch 'chat-sidebar' into fast-push100
101 // Folded sections, remembered per workspace on this device. A folded
102 // section still shows what is unread and the conversation open now.
103 const [folded, setFolded] = useState<ReadonlySet<string>>(new Set());
104 useEffect(() => setFolded(readFolded(slug)), [slug]);
105 const fold = (id: string) =>
106 setFolded((now) => {
107 const next = new Set(now);
108 if (!next.delete(id)) next.add(id);
109 writeFolded(slug, next);
110 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)111 });
Merge branch 'chat-sidebar' into fast-push112 const keepShown = (entry: ChatSidebarEntry) => {
113 const to = channelPath(slug, entry.channel);
114 return pathname === to || pathname.startsWith(`${to}/`) || (!entry.muted && (entry.unread > 0 || entry.mentions > 0));
115 };
Chat and workspace agents: channels, DMs and named agents you talk to116
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 /** The direct message with an agent: opened, or made on first use. */
118 const openAgent = async (agent: WorkspaceAgent, pin = false) => {
119 const had = agentDms.get(agent.id);
120 if (had && !pin) return navigate(channelPath(slug, had.channel));
121 setOpening(agent.id);
122 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}`] });
123 if (opened.ok && pin) await send({ intent: "preferences", channel_id: opened.value.channel.id, starred: !had?.starred });
124 setOpening(null);
125 if (opened.ok && !pin) navigate(opened.value.to);
126 if (pin) refresh();
127 };
128 const togglePin = async (entry: ChatSidebarEntry) => {
129 const next = !entry.starred;
130 setPins((now) => new Map(now).set(entry.channel.id, next));
131 const done = await send({ intent: "preferences", channel_id: entry.channel.id, starred: next });
132 if (!done.ok)
133 setPins((now) => {
134 const back = new Map(now);
135 back.delete(entry.channel.id);
136 return back;
137 });
138 };
139
140 const conversation = (entry: ChatSidebarEntry) => (
141 <ConversationRow key={entry.channel.id} entry={entry} slug={slug} pathname={pathname} agents={data?.agents ?? []} onPin={() => void togglePin(entry)} />
142 );
143
Chat and workspace agents: channels, DMs and named agents you talk to144 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 managers145 // 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.146 <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)147 {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.148 <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.149 <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.150 <div className="flex items-center gap-0.5">
151 <DndMenu />
152 <NewMessageButton slug={slug} />
153 </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)154 </div>
155 )}
Chat and workspace agents: channels, DMs and named agents you talk to156 <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.157 <div className="flex items-center gap-2">
158 <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">
159 <Search size={14} className="shrink-0 text-faint" />
160 <input
161 value={query}
162 onChange={(event) => setQuery(event.target.value)}
163 onKeyDown={(event) => {
164 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.165 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.166 if (first) {
167 navigate(channelPath(slug, first.channel));
168 setQuery("");
169 } else if (agents[0]) void openAgent(agents[0]);
170 } else if (event.key === "Escape") setQuery("");
171 }}
172 placeholder="Jump to channel or person"
173 aria-label="Jump to channel or person"
174 autoComplete="off"
175 data-1p-ignore
176 className="min-w-0 grow bg-transparent text-fg outline-none placeholder:text-faint"
177 />
178 </label>
179 {/* A phone has no heading row: Do not disturb sits by the search box instead. */}
180 {!heading && <DndMenu className="size-8 max-md:size-11" />}
181 </div>
Chat and workspace agents: channels, DMs and named agents you talk to182 <div role="radiogroup" aria-label="Show" className="grid grid-cols-3 rounded-md bg-surface p-0.5 ring-1 ring-line">
183 {(["all", "unread", "mentions"] as const).map((key) => (
184 <button
185 key={key}
186 type="button"
187 role="radio"
188 aria-checked={filter === key}
189 onClick={() => setFilter(key)}
190 className={`h-6 rounded-[5px] text-xs font-medium capitalize transition-colors ${
191 filter === key ? "bg-raised text-fg shadow-sm ring-1 ring-line-strong" : "text-muted hover:text-fg"
192 }`}
193 >
194 {key}
195 </button>
196 ))}
197 </div>
198 </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)199 {/* A phone's list (no heading): room at the end so the compose button never covers the last row. */}
200 <nav
201 aria-label="Conversations"
202 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))]"}`}
203 >
Chat and workspace agents: channels, DMs and named agents you talk to204 {loading ? (
205 <SidebarSkeleton />
206 ) : !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)207 <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 to208 ) : (
209 <>
Merge branch 'chat-sidebar' into fast-push210 {pinned.length > 0 && (
211 <Section id="pinned" title="Pinned" folded={folded} onFold={fold} keep={pinned.filter(keepShown).map(conversation)}>
212 {pinned.map(conversation)}
213 </Section>
214 )}
215 <Section
216 id="channels"
217 title="Channels"
218 folded={folded}
219 onFold={fold}
220 action={<CreateChannelButton slug={slug} />}
221 keep={channels.filter(keepShown).map(conversation)}
222 >
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)223 {channels.map(conversation)}
Chat and workspace agents: channels, DMs and named agents you talk to224 {channels.length === 0 && filter === "all" && !query && <Empty>No channels yet.</Empty>}
Merge branch 'chat-sidebar' into fast-push225 {filter === "all" && !query && (
226 <ExtraRow to={`/${slug}/-/chat/browse`} icon={<Compass size={15} />} count={sidebar.browsable > 0 ? sidebar.browsable : undefined} countLabel="to join">
227 Browse channels
228 </ExtraRow>
229 )}
Chat and workspace agents: channels, DMs and named agents you talk to230 </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)231 <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.232 id="people"
233 title="People"
Merge branch 'chat-sidebar' into fast-push234 folded={folded}
235 onFold={fold}
236 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.237 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)238 >
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.239 {people.map(conversation)}
240 {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)241 </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.242 {(agentConversations.length > 0 || agents.length > 0 || (filter === "all" && !query)) && (
Merge branch 'chat-sidebar' into fast-push243 <Section
244 id="agents"
245 title="Agents"
246 folded={folded}
247 onFold={fold}
248 action={
249 owner ? (
250 <Hint label="New agent">
251 <Link
252 to={`/${slug}/-/agents/new`}
253 aria-label="New agent"
254 className="flex size-7 items-center justify-center rounded-md text-faint transition-colors hover:bg-raised hover:text-fg"
255 >
256 <Plus size={14} />
257 </Link>
258 </Hint>
259 ) : null
260 }
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.261 keep={agentConversations.filter(keepShown).map(conversation)}
Merge branch 'chat-sidebar' into fast-push262 >
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.263 {agentConversations.map(conversation)}
264 {agentConversations.length === 0 && agents.length === 0 && filter === "all" && !query && <Empty>Message an agent.</Empty>}
Merge branch 'chat-sidebar' into fast-push265 {agents.map((agent) => (
266 <AgentRow
267 key={agent.id}
268 agent={agent}
269 busy={opening === agent.id}
270 onOpen={() => void openAgent(agent)}
271 onPin={() => void openAgent(agent, true)}
272 />
273 ))}
274 {filter === "all" && !query && (
275 <ExtraRow to={`/${slug}/-/agents`} icon={<Bot size={15} />}>
276 {(data?.agents ?? []).length === 0 ? (owner ? "Hire an agent" : "No agents yet") : "All agents"}
277 </ExtraRow>
278 )}
279 </Section>
280 )}
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)281 {shown.length === 0 && agents.length === 0 && (filter !== "all" || query) && (
282 <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 to283 )}
284 </>
285 )}
286 </nav>
287 </div>
288 );
289}
290
Merge branch 'chat-sidebar' into fast-push291const FOLDED_KEY = (slug: string) => `g1t:chat-folded:${slug}`;
292
293/** The sections folded in this workspace, as last left; none if storage is not there. */
294function readFolded(slug: string): ReadonlySet<string> {
295 try {
296 const saved = JSON.parse(localStorage.getItem(FOLDED_KEY(slug)) ?? "[]");
297 return new Set(Array.isArray(saved) ? saved.filter((id): id is string => typeof id === "string") : []);
298 } catch {
299 return new Set();
300 }
301}
302
303function writeFolded(slug: string, folded: ReadonlySet<string>) {
304 try {
305 if (folded.size === 0) localStorage.removeItem(FOLDED_KEY(slug));
306 else localStorage.setItem(FOLDED_KEY(slug), JSON.stringify([...folded]));
307 } catch {
308 // Private windows and blocked storage: folding still works until a reload.
309 }
310}
311
312/**
313 * A titled part of the sidebar that folds away. Folded, it keeps `keep`
314 * showing: the unread conversations and the open one.
315 */
316function Section({
317 id,
318 title,
319 folded,
320 onFold,
321 action,
322 keep,
323 children,
324}: {
325 id: string;
326 title: string;
327 folded: ReadonlySet<string>;
328 onFold: (id: string) => void;
329 action?: ReactNode;
330 keep?: ReactNode[];
331 children: ReactNode;
332}) {
333 const open = !folded.has(id);
334 const list = `chat-section-${id}`;
Chat and workspace agents: channels, DMs and named agents you talk to335 return (
336 <section className="mb-3">
337 <div className="group/head flex h-7 items-center justify-between pr-0.5 pl-1">
338 <button
339 type="button"
Merge branch 'chat-sidebar' into fast-push340 onClick={() => onFold(id)}
Chat and workspace agents: channels, DMs and named agents you talk to341 aria-expanded={open}
Merge branch 'chat-sidebar' into fast-push342 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.343 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 to344 >
Merge branch 'chat-sidebar' into fast-push345 <ChevronDown size={12} className={`shrink-0 transition-transform ${open ? "" : "-rotate-90"}`} />
Chat and workspace agents: channels, DMs and named agents you talk to346 {title}
347 </button>
Merge branch 'chat-sidebar' into fast-push348 <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 to349 </div>
Merge branch 'chat-sidebar' into fast-push350 {(open || (keep && keep.length > 0)) && (
351 <ul id={list} className="space-y-px">
352 {open ? children : keep}
353 </ul>
354 )}
Chat and workspace agents: channels, DMs and named agents you talk to355 </section>
356 );
357}
358
Merge branch 'chat-sidebar' into fast-push359/** A quieter row at the end of a section: somewhere to go rather than a conversation. */
360function ExtraRow({ to, icon, count, countLabel, children }: { to: string; icon: ReactNode; count?: number; countLabel?: string; children: ReactNode }) {
361 return (
362 <li>
363 <NavLink
364 to={to}
365 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.366 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-push367 >
368 <span className="flex w-4.5 shrink-0 justify-center">{icon}</span>
369 <span className="min-w-0 grow truncate">{children}</span>
370 {count != null && (
371 <span className="shrink-0 text-xs tabular-nums">
372 {count}
373 {countLabel && <span className="sr-only"> {countLabel}</span>}
374 </span>
375 )}
376 </NavLink>
377 </li>
378 );
379}
380
Chat and workspace agents: channels, DMs and named agents you talk to381function Empty({ children }: { children: ReactNode }) {
382 return <li className="px-2 py-1 text-xs text-faint">{children}</li>;
383}
384
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)385/** Pin or unpin, on hover or focus; the same as the context menu's first item. */
386function PinButton({ pinned, onPin, label }: { pinned: boolean; onPin: () => void; label: string }) {
387 return (
388 <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.389 <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)390 type="button"
391 aria-label={`${pinned ? "Unpin" : "Pin"} ${label}`}
392 onClick={(event) => {
393 event.preventDefault();
394 event.stopPropagation();
395 onPin();
396 }}
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.397 variant="secondary" size="icon"
398 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)399 >
400 {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.401 </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)402 </Hint>
403 );
404}
405
406/** Right-click on a row: pin it, or open it. */
407function RowMenu({ pinned, onPin, to, children }: { pinned: boolean; onPin: () => void; to?: string; children: ReactNode }) {
408 const navigate = useNavigate();
409 const item =
410 "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";
411 return (
412 <ContextMenu.Root>
413 <ContextMenu.Trigger asChild>{children}</ContextMenu.Trigger>
414 <ContextMenu.Portal>
415 <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">
416 <ContextMenu.Item className={item} onSelect={onPin}>
417 {pinned ? <PinOff /> : <Pin />}
418 {pinned ? "Unpin" : "Pin to the top"}
419 </ContextMenu.Item>
420 {to && (
421 <ContextMenu.Item className={item} onSelect={() => navigate(to)}>
422 <Hash />
423 Open
424 </ContextMenu.Item>
425 )}
426 </ContextMenu.Content>
427 </ContextMenu.Portal>
428 </ContextMenu.Root>
429 );
430}
431
432/**
433 * Reads a conversation ahead, on hover, focus or the start of a touch, so
434 * opening it draws at once (its route's `clientLoader` keeps it).
435 */
436function usePrefetchConversation(to: string) {
437 const fetcher = useFetcher();
438 const load = () => {
439 if (fetcher.state === "idle" && fetcher.data === undefined && !conversationCache.has(to)) fetcher.load(to);
440 };
441 return { onPointerEnter: load, onFocus: load, onTouchStart: load };
442}
443
444/** One channel or direct message: its mark, its name, its unread count, and a pin. */
445function ConversationRow({
446 entry,
447 slug,
448 pathname,
449 agents,
450 onPin,
451}: {
452 entry: ChatSidebarEntry;
453 slug: string;
454 pathname: string;
455 agents: WorkspaceAgent[];
456 onPin: () => void;
457}) {
Chat and workspace agents: channels, DMs and named agents you talk to458 const to = channelPath(slug, entry.channel);
459 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)460 const prefetch = usePrefetchConversation(to);
Chat and workspace agents: channels, DMs and named agents you talk to461 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)462 const agentId = agentDmOf(entry);
463 const agent = agentId ? agents.find((a) => a.id === agentId) : null;
Chat and workspace agents: channels, DMs and named agents you talk to464 const other = entry.others[0];
Merge branch 'chat-sidebar' into fast-push465 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.466 // A person and an agent together: both faces, the agent's marked, and the word.
467 const mixed = mixedDm(entry);
An archived agent can be found, brought back, or deleted for good. The Agents page ends with Archived, folded, where owners see every archived agent and members their own personal ones, each with when and by whom it was archived and what deleting it would take; Restore brings it back, under a new handle when a live agent has taken its old one, and Delete permanently asks for the handle to be typed and then removes its sessions, replies, memories, routines, versions, drafts, skills and its computer's home, leaving its messages in chat under its saved name, the workspace's spend totals and the audit log. In Chat an archived agent is deactivated the way people expect: never offered as someone to message or mention, never woken, a conversation with it kept in the list, dimmed and marked Archived, open to read with a note in place of the composer and a way to restore it for owners; after deletion the conversation still reads, its messages marked as a deleted agent's. The agents, chat and agent budgets guides say so.468 // With an archived agent (lib/chat.ts): still here to read, dimmed, and said to be archived in place of its title.
469 const archived = archivedAgentsDm(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)470 const icon = agent ? (
Merge branch 'chat-sidebar' into fast-push471 <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.472 ) : mixed ? (
473 <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)474 ) : 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 managers475 // One person: their dot, cut out of the sidebar behind it.
476 <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)477 ) : entry.channel.private ? (
478 <Lock size={14} />
479 ) : (
480 <Hash size={15} />
481 );
482 return (
483 <li className="group/row relative">
484 <RowMenu pinned={entry.starred} onPin={onPin} to={to}>
485 <NavLink
486 to={to}
487 {...prefetch}
488 aria-current={current ? "page" : undefined}
489 className={`${ROW} ${
An archived agent can be found, brought back, or deleted for good. The Agents page ends with Archived, folded, where owners see every archived agent and members their own personal ones, each with when and by whom it was archived and what deleting it would take; Restore brings it back, under a new handle when a live agent has taken its old one, and Delete permanently asks for the handle to be typed and then removes its sessions, replies, memories, routines, versions, drafts, skills and its computer's home, leaving its messages in chat under its saved name, the workspace's spend totals and the audit log. In Chat an archived agent is deactivated the way people expect: never offered as someone to message or mention, never woken, a conversation with it kept in the list, dimmed and marked Archived, open to read with a note in place of the composer and a way to restore it for owners; after deletion the conversation still reads, its messages marked as a deleted agent's. The agents, chat and agent budgets guides say so.490 current ? "bg-sidebar-accent text-fg" : unread ? "text-fg hover:bg-sidebar-accent/60" : entry.muted || archived ? "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)491 }`}
492 >
An archived agent can be found, brought back, or deleted for good. The Agents page ends with Archived, folded, where owners see every archived agent and members their own personal ones, each with when and by whom it was archived and what deleting it would take; Restore brings it back, under a new handle when a live agent has taken its old one, and Delete permanently asks for the handle to be typed and then removes its sessions, replies, memories, routines, versions, drafts, skills and its computer's home, leaving its messages in chat under its saved name, the workspace's spend totals and the audit log. In Chat an archived agent is deactivated the way people expect: never offered as someone to message or mention, never woken, a conversation with it kept in the list, dimmed and marked Archived, open to read with a note in place of the composer and a way to restore it for owners; after deletion the conversation still reads, its messages marked as a deleted agent's. The agents, chat and agent budgets guides say so.493 <span className={`flex shrink-0 justify-center ${mixed ? "w-7" : "w-4.5"} ${current || unread ? "text-muted" : "text-faint"} ${archived ? "opacity-60" : ""}`}>{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 managers494 <span className={`min-w-0 grow truncate ${unread ? "font-semibold" : current ? "font-medium" : ""}`}>
Merge branch 'chat-sidebar' into fast-push495 {agent ? agent.display_name : entry.title}
496 {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 managers497 {entry.channel.kind === "dm" && other?.kind === "user" && entry.others.length === 1 && (
498 <PersonStatusEmoji person={{ id: other.id, username: other.name }} size={13} className="ml-1.5 align-[-2px]" inert />
499 )}
500 </span>
An archived agent can be found, brought back, or deleted for good. The Agents page ends with Archived, folded, where owners see every archived agent and members their own personal ones, each with when and by whom it was archived and what deleting it would take; Restore brings it back, under a new handle when a live agent has taken its old one, and Delete permanently asks for the handle to be typed and then removes its sessions, replies, memories, routines, versions, drafts, skills and its computer's home, leaving its messages in chat under its saved name, the workspace's spend totals and the audit log. In Chat an archived agent is deactivated the way people expect: never offered as someone to message or mention, never woken, a conversation with it kept in the list, dimmed and marked Archived, open to read with a note in place of the composer and a way to restore it for owners; after deletion the conversation still reads, its messages marked as a deleted agent's. The agents, chat and agent budgets guides say so.501 {archived && (
502 <Badge tone="neutral" className="shrink-0 px-1.5 font-normal">
503 {archived.deleted ? "Deleted" : "Archived"}
504 </Badge>
505 )}
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.506 {mixed && (
507 <Badge tone="accent" className="shrink-0 px-1.5 font-normal">
508 with an agent
509 </Badge>
510 )}
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)511 {entry.muted && <BellOff size={12} className="shrink-0 text-faint" aria-label="Muted" />}
512 <Count entry={entry} />
513 </NavLink>
514 </RowMenu>
515 <PinButton pinned={entry.starred} onPin={onPin} label={entry.title} />
516 </li>
517 );
518}
519
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.520/**
521 * An agent's face in a row, the agent marker ringed in the sidebar's own
522 * colour; any but g1t carries its status in the other corner.
523 */
Merge branch 'chat-sidebar' into fast-push524function AgentFace({ agent, g1t }: { agent: WorkspaceAgent; g1t: boolean }) {
525 return (
526 <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.527 <AgentAvatar agent={{ ...agent, builtin: g1t }} size={18} ring="var(--chat-sidebar-bg)" />
Merge branch 'chat-sidebar' into fast-push528 {!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.529 <span className="absolute -top-1 -right-1 flex rounded-full p-[2px]" style={{ background: "var(--chat-sidebar-bg)" }}>
Merge branch 'chat-sidebar' into fast-push530 <StatusDot status={agent.status} className="size-[7px] shadow-none" />
531 </span>
532 )}
533 </span>
534 );
535}
536
537/** An agent you have not talked to yet: its face, name and title. Opening it starts your conversation. */
538function 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)539 const g1t = isOrchestrator(agent);
Chat and workspace agents: channels, DMs and named agents you talk to540 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)541 <li className="group/row relative">
Merge branch 'chat-sidebar' into fast-push542 <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)543 <button
544 type="button"
545 onClick={onOpen}
546 aria-busy={busy || undefined}
Merge branch 'chat-sidebar' into fast-push547 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.548 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)549 >
Merge branch 'chat-sidebar' into fast-push550 <span className="flex w-4.5 shrink-0 justify-center">
551 <AgentFace agent={agent} g1t={g1t} />
Chat and workspace agents: channels, DMs and named agents you talk to552 </span>
Merge branch 'chat-sidebar' into fast-push553 <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)554 {agent.display_name}
Merge branch 'chat-sidebar' into fast-push555 <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)556 </span>
557 </button>
558 </RowMenu>
Merge branch 'chat-sidebar' into fast-push559 <PinButton pinned={false} onPin={onPin} label={agent.display_name} />
Chat and workspace agents: channels, DMs and named agents you talk to560 </li>
561 );
562}
563
564function SidebarSkeleton() {
565 return (
566 <div className="space-y-2 px-2 pt-1" aria-busy="true">
567 <Skeleton className="h-3 w-16" />
568 {[70, 52, 84, 60].map((w) => (
569 <div key={w} className="flex h-6 items-center gap-2">
570 <Skeleton className="size-4 rounded" />
571 <Skeleton className="h-3" style={{ width: `${w}%` }} />
572 </div>
573 ))}
574 <Skeleton className="mt-4 h-3 w-24" />
575 {[64, 48, 72].map((w) => (
576 <div key={w} className="flex h-6 items-center gap-2">
577 <Skeleton className="size-4 rounded-full" />
578 <Skeleton className="h-3" style={{ width: `${w}%` }} />
579 </div>
580 ))}
581 </div>
582 );
583}

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