Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
585 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));
A conversation with an archived agent leaves Chat's list. Until the agent is restored or deleted for good, its direct message is found by typing its name into Jump to channel or person, and it stays listed only while it is the conversation you are in; the chat and agents guides say so.89 // The open conversation stays listed even when it is one with an archived agent, which Jump to otherwise finds.
90 const currentId = entries.find((e) => pathname === channelPath(slug, e.channel) || pathname.startsWith(`${channelPath(slug, e.channel)}/`))?.channel.id ?? null;
91 const shown = filterEntries(entries, filter, query, currentId);
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.92 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)93 const { agentDms } = sections(entries);
Chat and workspace agents: channels, DMs and named agents you talk to94 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)95 const owner = data?.role === "owner";
96
Merge branch 'chat-sidebar' into fast-push97 // The agents you have not talked to yet, g1t first: one click opens a
98 // conversation, which then lists under Direct messages like anyone's.
99 // 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.100 // An archived agent is never offered (lib/chat.ts `agentsToTalkTo`).
101 const agents = filter !== "all" ? [] : agentsToTalkTo(data?.agents ?? [], agentDms, query);
Merge branch 'chat-sidebar' into fast-push102
103 // Folded sections, remembered per workspace on this device. A folded
104 // section still shows what is unread and the conversation open now.
105 const [folded, setFolded] = useState<ReadonlySet<string>>(new Set());
106 useEffect(() => setFolded(readFolded(slug)), [slug]);
107 const fold = (id: string) =>
108 setFolded((now) => {
109 const next = new Set(now);
110 if (!next.delete(id)) next.add(id);
111 writeFolded(slug, next);
112 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)113 });
Merge branch 'chat-sidebar' into fast-push114 const keepShown = (entry: ChatSidebarEntry) => {
115 const to = channelPath(slug, entry.channel);
116 return pathname === to || pathname.startsWith(`${to}/`) || (!entry.muted && (entry.unread > 0 || entry.mentions > 0));
117 };
Chat and workspace agents: channels, DMs and named agents you talk to118
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)119 /** The direct message with an agent: opened, or made on first use. */
120 const openAgent = async (agent: WorkspaceAgent, pin = false) => {
121 const had = agentDms.get(agent.id);
122 if (had && !pin) return navigate(channelPath(slug, had.channel));
123 setOpening(agent.id);
124 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}`] });
125 if (opened.ok && pin) await send({ intent: "preferences", channel_id: opened.value.channel.id, starred: !had?.starred });
126 setOpening(null);
127 if (opened.ok && !pin) navigate(opened.value.to);
128 if (pin) refresh();
129 };
130 const togglePin = async (entry: ChatSidebarEntry) => {
131 const next = !entry.starred;
132 setPins((now) => new Map(now).set(entry.channel.id, next));
133 const done = await send({ intent: "preferences", channel_id: entry.channel.id, starred: next });
134 if (!done.ok)
135 setPins((now) => {
136 const back = new Map(now);
137 back.delete(entry.channel.id);
138 return back;
139 });
140 };
141
142 const conversation = (entry: ChatSidebarEntry) => (
143 <ConversationRow key={entry.channel.id} entry={entry} slug={slug} pathname={pathname} agents={data?.agents ?? []} onPin={() => void togglePin(entry)} />
144 );
145
Chat and workspace agents: channels, DMs and named agents you talk to146 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 managers147 // 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.148 <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)149 {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.150 <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.151 <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.152 <div className="flex items-center gap-0.5">
153 <DndMenu />
154 <NewMessageButton slug={slug} />
155 </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)156 </div>
157 )}
Chat and workspace agents: channels, DMs and named agents you talk to158 <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.159 <div className="flex items-center gap-2">
160 <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">
161 <Search size={14} className="shrink-0 text-faint" />
162 <input
163 value={query}
164 onChange={(event) => setQuery(event.target.value)}
165 onKeyDown={(event) => {
166 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.167 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.168 if (first) {
169 navigate(channelPath(slug, first.channel));
170 setQuery("");
171 } else if (agents[0]) void openAgent(agents[0]);
172 } else if (event.key === "Escape") setQuery("");
173 }}
174 placeholder="Jump to channel or person"
175 aria-label="Jump to channel or person"
176 autoComplete="off"
177 data-1p-ignore
178 className="min-w-0 grow bg-transparent text-fg outline-none placeholder:text-faint"
179 />
180 </label>
181 {/* A phone has no heading row: Do not disturb sits by the search box instead. */}
182 {!heading && <DndMenu className="size-8 max-md:size-11" />}
183 </div>
Chat and workspace agents: channels, DMs and named agents you talk to184 <div role="radiogroup" aria-label="Show" className="grid grid-cols-3 rounded-md bg-surface p-0.5 ring-1 ring-line">
185 {(["all", "unread", "mentions"] as const).map((key) => (
186 <button
187 key={key}
188 type="button"
189 role="radio"
190 aria-checked={filter === key}
191 onClick={() => setFilter(key)}
192 className={`h-6 rounded-[5px] text-xs font-medium capitalize transition-colors ${
193 filter === key ? "bg-raised text-fg shadow-sm ring-1 ring-line-strong" : "text-muted hover:text-fg"
194 }`}
195 >
196 {key}
197 </button>
198 ))}
199 </div>
200 </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)201 {/* A phone's list (no heading): room at the end so the compose button never covers the last row. */}
202 <nav
203 aria-label="Conversations"
204 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))]"}`}
205 >
Chat and workspace agents: channels, DMs and named agents you talk to206 {loading ? (
207 <SidebarSkeleton />
208 ) : !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)209 <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 to210 ) : (
211 <>
Merge branch 'chat-sidebar' into fast-push212 {pinned.length > 0 && (
213 <Section id="pinned" title="Pinned" folded={folded} onFold={fold} keep={pinned.filter(keepShown).map(conversation)}>
214 {pinned.map(conversation)}
215 </Section>
216 )}
217 <Section
218 id="channels"
219 title="Channels"
220 folded={folded}
221 onFold={fold}
222 action={<CreateChannelButton slug={slug} />}
223 keep={channels.filter(keepShown).map(conversation)}
224 >
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)225 {channels.map(conversation)}
Chat and workspace agents: channels, DMs and named agents you talk to226 {channels.length === 0 && filter === "all" && !query && <Empty>No channels yet.</Empty>}
Merge branch 'chat-sidebar' into fast-push227 {filter === "all" && !query && (
228 <ExtraRow to={`/${slug}/-/chat/browse`} icon={<Compass size={15} />} count={sidebar.browsable > 0 ? sidebar.browsable : undefined} countLabel="to join">
229 Browse channels
230 </ExtraRow>
231 )}
Chat and workspace agents: channels, DMs and named agents you talk to232 </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)233 <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.234 id="people"
235 title="People"
Merge branch 'chat-sidebar' into fast-push236 folded={folded}
237 onFold={fold}
238 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.239 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)240 >
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.241 {people.map(conversation)}
242 {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)243 </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.244 {(agentConversations.length > 0 || agents.length > 0 || (filter === "all" && !query)) && (
Merge branch 'chat-sidebar' into fast-push245 <Section
246 id="agents"
247 title="Agents"
248 folded={folded}
249 onFold={fold}
250 action={
251 owner ? (
252 <Hint label="New agent">
253 <Link
254 to={`/${slug}/-/agents/new`}
255 aria-label="New agent"
256 className="flex size-7 items-center justify-center rounded-md text-faint transition-colors hover:bg-raised hover:text-fg"
257 >
258 <Plus size={14} />
259 </Link>
260 </Hint>
261 ) : null
262 }
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 keep={agentConversations.filter(keepShown).map(conversation)}
Merge branch 'chat-sidebar' into fast-push264 >
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.265 {agentConversations.map(conversation)}
266 {agentConversations.length === 0 && agents.length === 0 && filter === "all" && !query && <Empty>Message an agent.</Empty>}
Merge branch 'chat-sidebar' into fast-push267 {agents.map((agent) => (
268 <AgentRow
269 key={agent.id}
270 agent={agent}
271 busy={opening === agent.id}
272 onOpen={() => void openAgent(agent)}
273 onPin={() => void openAgent(agent, true)}
274 />
275 ))}
276 {filter === "all" && !query && (
277 <ExtraRow to={`/${slug}/-/agents`} icon={<Bot size={15} />}>
278 {(data?.agents ?? []).length === 0 ? (owner ? "Hire an agent" : "No agents yet") : "All agents"}
279 </ExtraRow>
280 )}
281 </Section>
282 )}
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)283 {shown.length === 0 && agents.length === 0 && (filter !== "all" || query) && (
284 <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 to285 )}
286 </>
287 )}
288 </nav>
289 </div>
290 );
291}
292
Merge branch 'chat-sidebar' into fast-push293const FOLDED_KEY = (slug: string) => `g1t:chat-folded:${slug}`;
294
295/** The sections folded in this workspace, as last left; none if storage is not there. */
296function readFolded(slug: string): ReadonlySet<string> {
297 try {
298 const saved = JSON.parse(localStorage.getItem(FOLDED_KEY(slug)) ?? "[]");
299 return new Set(Array.isArray(saved) ? saved.filter((id): id is string => typeof id === "string") : []);
300 } catch {
301 return new Set();
302 }
303}
304
305function writeFolded(slug: string, folded: ReadonlySet<string>) {
306 try {
307 if (folded.size === 0) localStorage.removeItem(FOLDED_KEY(slug));
308 else localStorage.setItem(FOLDED_KEY(slug), JSON.stringify([...folded]));
309 } catch {
310 // Private windows and blocked storage: folding still works until a reload.
311 }
312}
313
314/**
315 * A titled part of the sidebar that folds away. Folded, it keeps `keep`
316 * showing: the unread conversations and the open one.
317 */
318function Section({
319 id,
320 title,
321 folded,
322 onFold,
323 action,
324 keep,
325 children,
326}: {
327 id: string;
328 title: string;
329 folded: ReadonlySet<string>;
330 onFold: (id: string) => void;
331 action?: ReactNode;
332 keep?: ReactNode[];
333 children: ReactNode;
334}) {
335 const open = !folded.has(id);
336 const list = `chat-section-${id}`;
Chat and workspace agents: channels, DMs and named agents you talk to337 return (
338 <section className="mb-3">
339 <div className="group/head flex h-7 items-center justify-between pr-0.5 pl-1">
340 <button
341 type="button"
Merge branch 'chat-sidebar' into fast-push342 onClick={() => onFold(id)}
Chat and workspace agents: channels, DMs and named agents you talk to343 aria-expanded={open}
Merge branch 'chat-sidebar' into fast-push344 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.345 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 to346 >
Merge branch 'chat-sidebar' into fast-push347 <ChevronDown size={12} className={`shrink-0 transition-transform ${open ? "" : "-rotate-90"}`} />
Chat and workspace agents: channels, DMs and named agents you talk to348 {title}
349 </button>
Merge branch 'chat-sidebar' into fast-push350 <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 to351 </div>
Merge branch 'chat-sidebar' into fast-push352 {(open || (keep && keep.length > 0)) && (
353 <ul id={list} className="space-y-px">
354 {open ? children : keep}
355 </ul>
356 )}
Chat and workspace agents: channels, DMs and named agents you talk to357 </section>
358 );
359}
360
Merge branch 'chat-sidebar' into fast-push361/** A quieter row at the end of a section: somewhere to go rather than a conversation. */
362function ExtraRow({ to, icon, count, countLabel, children }: { to: string; icon: ReactNode; count?: number; countLabel?: string; children: ReactNode }) {
363 return (
364 <li>
365 <NavLink
366 to={to}
367 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.368 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-push369 >
370 <span className="flex w-4.5 shrink-0 justify-center">{icon}</span>
371 <span className="min-w-0 grow truncate">{children}</span>
372 {count != null && (
373 <span className="shrink-0 text-xs tabular-nums">
374 {count}
375 {countLabel && <span className="sr-only"> {countLabel}</span>}
376 </span>
377 )}
378 </NavLink>
379 </li>
380 );
381}
382
Chat and workspace agents: channels, DMs and named agents you talk to383function Empty({ children }: { children: ReactNode }) {
384 return <li className="px-2 py-1 text-xs text-faint">{children}</li>;
385}
386
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)387/** Pin or unpin, on hover or focus; the same as the context menu's first item. */
388function PinButton({ pinned, onPin, label }: { pinned: boolean; onPin: () => void; label: string }) {
389 return (
390 <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.391 <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)392 type="button"
393 aria-label={`${pinned ? "Unpin" : "Pin"} ${label}`}
394 onClick={(event) => {
395 event.preventDefault();
396 event.stopPropagation();
397 onPin();
398 }}
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.399 variant="secondary" size="icon"
400 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)401 >
402 {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.403 </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)404 </Hint>
405 );
406}
407
408/** Right-click on a row: pin it, or open it. */
409function RowMenu({ pinned, onPin, to, children }: { pinned: boolean; onPin: () => void; to?: string; children: ReactNode }) {
410 const navigate = useNavigate();
411 const item =
412 "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";
413 return (
414 <ContextMenu.Root>
415 <ContextMenu.Trigger asChild>{children}</ContextMenu.Trigger>
416 <ContextMenu.Portal>
417 <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">
418 <ContextMenu.Item className={item} onSelect={onPin}>
419 {pinned ? <PinOff /> : <Pin />}
420 {pinned ? "Unpin" : "Pin to the top"}
421 </ContextMenu.Item>
422 {to && (
423 <ContextMenu.Item className={item} onSelect={() => navigate(to)}>
424 <Hash />
425 Open
426 </ContextMenu.Item>
427 )}
428 </ContextMenu.Content>
429 </ContextMenu.Portal>
430 </ContextMenu.Root>
431 );
432}
433
434/**
435 * Reads a conversation ahead, on hover, focus or the start of a touch, so
436 * opening it draws at once (its route's `clientLoader` keeps it).
437 */
438function usePrefetchConversation(to: string) {
439 const fetcher = useFetcher();
440 const load = () => {
441 if (fetcher.state === "idle" && fetcher.data === undefined && !conversationCache.has(to)) fetcher.load(to);
442 };
443 return { onPointerEnter: load, onFocus: load, onTouchStart: load };
444}
445
446/** One channel or direct message: its mark, its name, its unread count, and a pin. */
447function ConversationRow({
448 entry,
449 slug,
450 pathname,
451 agents,
452 onPin,
453}: {
454 entry: ChatSidebarEntry;
455 slug: string;
456 pathname: string;
457 agents: WorkspaceAgent[];
458 onPin: () => void;
459}) {
Chat and workspace agents: channels, DMs and named agents you talk to460 const to = channelPath(slug, entry.channel);
461 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)462 const prefetch = usePrefetchConversation(to);
Chat and workspace agents: channels, DMs and named agents you talk to463 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)464 const agentId = agentDmOf(entry);
465 const agent = agentId ? agents.find((a) => a.id === agentId) : null;
Chat and workspace agents: channels, DMs and named agents you talk to466 const other = entry.others[0];
Merge branch 'chat-sidebar' into fast-push467 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.468 // A person and an agent together: both faces, the agent's marked, and the word.
469 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.470 // With an archived agent (lib/chat.ts): still here to read, dimmed, and said to be archived in place of its title.
471 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)472 const icon = agent ? (
Merge branch 'chat-sidebar' into fast-push473 <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.474 ) : mixed ? (
475 <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)476 ) : 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 managers477 // One person: their dot, cut out of the sidebar behind it.
478 <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)479 ) : entry.channel.private ? (
480 <Lock size={14} />
481 ) : (
482 <Hash size={15} />
483 );
484 return (
485 <li className="group/row relative">
486 <RowMenu pinned={entry.starred} onPin={onPin} to={to}>
487 <NavLink
488 to={to}
489 {...prefetch}
490 aria-current={current ? "page" : undefined}
491 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.492 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)493 }`}
494 >
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.495 <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 managers496 <span className={`min-w-0 grow truncate ${unread ? "font-semibold" : current ? "font-medium" : ""}`}>
Merge branch 'chat-sidebar' into fast-push497 {agent ? agent.display_name : entry.title}
498 {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 managers499 {entry.channel.kind === "dm" && other?.kind === "user" && entry.others.length === 1 && (
500 <PersonStatusEmoji person={{ id: other.id, username: other.name }} size={13} className="ml-1.5 align-[-2px]" inert />
501 )}
502 </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.503 {archived && (
504 <Badge tone="neutral" className="shrink-0 px-1.5 font-normal">
505 {archived.deleted ? "Deleted" : "Archived"}
506 </Badge>
507 )}
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.508 {mixed && (
509 <Badge tone="accent" className="shrink-0 px-1.5 font-normal">
510 with an agent
511 </Badge>
512 )}
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)513 {entry.muted && <BellOff size={12} className="shrink-0 text-faint" aria-label="Muted" />}
514 <Count entry={entry} />
515 </NavLink>
516 </RowMenu>
517 <PinButton pinned={entry.starred} onPin={onPin} label={entry.title} />
518 </li>
519 );
520}
521
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.522/**
523 * An agent's face in a row, the agent marker ringed in the sidebar's own
524 * colour; any but g1t carries its status in the other corner.
525 */
Merge branch 'chat-sidebar' into fast-push526function AgentFace({ agent, g1t }: { agent: WorkspaceAgent; g1t: boolean }) {
527 return (
528 <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.529 <AgentAvatar agent={{ ...agent, builtin: g1t }} size={18} ring="var(--chat-sidebar-bg)" />
Merge branch 'chat-sidebar' into fast-push530 {!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.531 <span className="absolute -top-1 -right-1 flex rounded-full p-[2px]" style={{ background: "var(--chat-sidebar-bg)" }}>
Merge branch 'chat-sidebar' into fast-push532 <StatusDot status={agent.status} className="size-[7px] shadow-none" />
533 </span>
534 )}
535 </span>
536 );
537}
538
539/** An agent you have not talked to yet: its face, name and title. Opening it starts your conversation. */
540function 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)541 const g1t = isOrchestrator(agent);
Chat and workspace agents: channels, DMs and named agents you talk to542 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)543 <li className="group/row relative">
Merge branch 'chat-sidebar' into fast-push544 <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)545 <button
546 type="button"
547 onClick={onOpen}
548 aria-busy={busy || undefined}
Merge branch 'chat-sidebar' into fast-push549 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.550 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)551 >
Merge branch 'chat-sidebar' into fast-push552 <span className="flex w-4.5 shrink-0 justify-center">
553 <AgentFace agent={agent} g1t={g1t} />
Chat and workspace agents: channels, DMs and named agents you talk to554 </span>
Merge branch 'chat-sidebar' into fast-push555 <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)556 {agent.display_name}
Merge branch 'chat-sidebar' into fast-push557 <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)558 </span>
559 </button>
560 </RowMenu>
Merge branch 'chat-sidebar' into fast-push561 <PinButton pinned={false} onPin={onPin} label={agent.display_name} />
Chat and workspace agents: channels, DMs and named agents you talk to562 </li>
563 );
564}
565
566function SidebarSkeleton() {
567 return (
568 <div className="space-y-2 px-2 pt-1" aria-busy="true">
569 <Skeleton className="h-3 w-16" />
570 {[70, 52, 84, 60].map((w) => (
571 <div key={w} className="flex h-6 items-center gap-2">
572 <Skeleton className="size-4 rounded" />
573 <Skeleton className="h-3" style={{ width: `${w}%` }} />
574 </div>
575 ))}
576 <Skeleton className="mt-4 h-3 w-24" />
577 {[64, 48, 72].map((w) => (
578 <div key={w} className="flex h-6 items-center gap-2">
579 <Skeleton className="size-4 rounded-full" />
580 <Skeleton className="h-3" style={{ width: `${w}%` }} />
581 </div>
582 ))}
583 </div>
584 );
585}

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