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
295 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.

The rail itself ships with the navigation it was committed without: the file that draws it was renamed from the dock and left out of the last change.1import { Check, ChevronDown, CircleUserRound, LayoutGrid, Plus, Settings } from "lucide-react";
2import { type ReactNode, useState } from "react";
3import { Link, useLocation, useNavigation } from "react-router";
4
5import { type Membership, type User, hasCodeAccess, shownUsername } from "@g1t/contracts";
6
7import { AppsLauncher, appIcon, pinnedApp, useAppPins } from "./apps";
8import { Mark } from "./logo";
9
10import { Avatar } from "./ui/avatar";
11import { Card } from "./ui/card";
12import { Kbd } from "./ui/kbd";
13import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog";
14import {
15 DropdownMenu,
16 DropdownMenuContent,
17 DropdownMenuItem,
18 DropdownMenuLabel,
19 DropdownMenuSeparator,
20 DropdownMenuTrigger,
21} from "./ui/dropdown-menu";
22import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
23import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarSeparator } from "./ui/sidebar";
24import { type BuiltinApp, type PinnableApp } from "../lib/apps";
25import { cn } from "../lib/cn";
26import { paletteKeyLabel } from "../lib/shortcut";
27import { type ModeKey, homePagePath, modeHome, modeOf } from "../lib/workspace-nav";
28
29type Item = { key: ModeKey; label: string; badge?: { count: number; loud: boolean } | null };
30
31/** A count on an icon: loud (the accent) for what waits on you, quiet for the rest. */
32export function CountBadge({ count, loud, className }: { count: number; loud: boolean; className?: string }) {
33 if (count <= 0) return null;
34 return (
35 <span
36 className={cn(
37 "absolute -top-1 -right-1.5 min-w-[1.125rem] rounded-full px-1 text-center text-[0.625rem] leading-[1.125rem] font-bold tabular-nums ring-2 ring-rail",
38 loud ? "bg-accent text-bg" : "bg-line-strong text-fg",
39 className,
40 )}
41 >
42 {count > 99 ? "99+" : count}
43 </span>
44 );
45}
46
47/** One icon of the rail: a link to an app, its name as the hint beside it, and what is unread on it. */
48function RailLink({ to, label, icon, current, badge }: { to: string; label: string; icon: ReactNode; current: boolean; badge?: Item["badge"] }) {
49 return (
50 <SidebarMenuItem>
51 <SidebarMenuButton asChild size="icon" isActive={current} tooltip={label}>
52 <Link to={to} prefetch="intent" aria-current={current ? "page" : undefined} aria-label={badge && badge.count > 0 ? `${label}, ${badge.count} unread` : label}>
53 {icon}
54 {badge && <CountBadge count={badge.count} loud={badge.loud} />}
55 </Link>
56 </SidebarMenuButton>
57 </SidebarMenuItem>
58 );
59}
60
61function displayName(membership: Membership): string {
62 return membership.name?.trim() || membership.slug;
63}
64
65/**
66 * The workspace you are in and the way to another: its avatar, its name
67 * and a chevron, opening your workspaces, a new one, and your own account.
68 * `row` heads the sidebar; `compact` starts the page header's trail when
69 * no sidebar is open.
70 */
71export function WorkspaceSwitcher({ user, workspace, compact = false }: { user: User; workspace: Membership; compact?: boolean }) {
72 return (
73 <DropdownMenu>
74 <DropdownMenuTrigger
75 aria-label={`${displayName(workspace)}: switch workspace`}
76 className={cn(
77 "flex min-w-0 items-center rounded-lg outline-none transition-colors hover:bg-sidebar-accent focus-visible:ring-2 focus-visible:ring-accent data-[state=open]:bg-sidebar-accent",
78 compact ? "h-8 shrink-0 gap-1.5 px-1 max-md:h-11 max-sm:gap-1 hover:bg-raised data-[state=open]:bg-raised" : "h-9 grow gap-2 px-1.5",
79 )}
80 >
81 <Avatar name={workspace.slug} image={workspace.avatar} size={compact ? 22 : 26} square />
82 <span className={cn("min-w-0 truncate font-semibold", compact ? "text-sm max-sm:hidden" : "text-[0.875rem]")}>{displayName(workspace)}</span>
83 <ChevronDown size={14} className="shrink-0 text-faint" />
84 </DropdownMenuTrigger>
85 <DropdownMenuContent align="start" className="w-72 p-1.5">
86 <DropdownMenuItem asChild className="gap-3 px-2 py-2">
87 <Link to={`/${workspace.slug}/-/workspace`}>
88 <Avatar name={workspace.slug} image={workspace.avatar} size={36} square />
89 <span className="flex min-w-0 grow flex-col leading-tight">
90 <span className="truncate text-sm font-medium text-fg">{displayName(workspace)}</span>
91 <span className="truncate font-mono text-xs text-muted">g1t.sh/{workspace.slug}</span>
92 <span className="mt-0.5 text-xs text-faint capitalize">{workspace.role}</span>
93 </span>
94 </Link>
95 </DropdownMenuItem>
96 <DropdownMenuSeparator className="my-1.5" />
97 <DropdownMenuLabel>Switch workspace</DropdownMenuLabel>
98 {(user.workspaces ?? []).map((membership) => (
99 <DropdownMenuItem key={membership.slug} asChild>
100 <Link to={homePagePath(membership.slug)}>
101 <Avatar name={membership.slug} image={membership.avatar} size={24} square />
102 <span className="flex min-w-0 grow flex-col leading-tight">
103 <span className="truncate">{displayName(membership)}</span>
104 <span className="truncate font-mono text-[0.6875rem] text-faint">{membership.slug}</span>
105 </span>
106 {membership.slug === workspace.slug ? <Check className="shrink-0 text-accent" /> : <span className="size-4 shrink-0" aria-hidden="true" />}
107 </Link>
108 </DropdownMenuItem>
109 ))}
110 <DropdownMenuItem asChild>
111 <Link to="/workspaces/new">
112 <Plus />
113 New workspace
114 </Link>
115 </DropdownMenuItem>
116 <DropdownMenuSeparator className="my-1.5" />
117 <DropdownMenuLabel>Personal account</DropdownMenuLabel>
118 <DropdownMenuItem asChild>
119 <Link to={`/u/${user.username}`}>
120 <CircleUserRound />
121 <span className="grow">Your profile</span>
122 <span className="font-mono text-xs text-faint">@{shownUsername(user)}</span>
123 </Link>
124 </DropdownMenuItem>
125 <DropdownMenuItem asChild>
126 <Link to="/settings">
127 <Settings />
128 Your settings
129 </Link>
130 </DropdownMenuItem>
131 </DropdownMenuContent>
132 </DropdownMenu>
133 );
134}
135
136/** Keyboard shortcuts, as the account menu lists them. */
137function shortcuts(palette: string, sidebar: string): [string, string][] {
138 return [
139 [palette, "Search, jump anywhere, or run a command"],
140 [sidebar, "Show or hide the sidebar"],
141 ["Esc", "Close a menu, dialog or panel"],
142 ["Enter", "Send a message in Chat"],
143 ["Shift Enter", "A new line in a message"],
The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404.144 ["@", "Mention a person or a team"],
The rail itself ships with the navigation it was committed without: the file that draws it was renamed from the dock and left out of the last change.145 ["↑ ↓ then Tab", "Choose a suggestion"],
146 ["Alt ↑ ↓", "Move a pinned project in Code's sidebar"],
147 ];
148}
149
150/** The sidebar's shortcut, as this computer writes it. */
151export function sidebarKeyLabel(platform: string | null): string {
152 return paletteKeyLabel(platform).replace(/K$/, "B");
153}
154
155/** The keyboard's shortcuts, opened from the account menu. */
156export function ShortcutsDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) {
157 const platform = typeof navigator === "undefined" ? null : navigator.platform;
158 return (
159 <Dialog open={open} onOpenChange={onOpenChange}>
160 <DialogContent className="max-w-md">
161 <DialogHeader>
162 <DialogTitle>Keyboard shortcuts</DialogTitle>
163 <DialogDescription>Everywhere in g1t, and in Chat.</DialogDescription>
164 </DialogHeader>
165 <Card asChild tone="plain" radius="lg" divided>
166 <dl>
167 {shortcuts(paletteKeyLabel(platform), sidebarKeyLabel(platform)).map(([key, what]) => (
168 <div key={key} className="flex items-center justify-between gap-4 px-3.5 py-2.5 text-sm">
169 <dt className="text-fg-soft">{what}</dt>
170 <dd>
171 <Kbd className="px-1.5 py-0.5 font-sans text-xs whitespace-nowrap text-fg ring-line-strong">{key}</Kbd>
172 </dd>
173 </div>
174 ))}
175 </dl>
176 </Card>
177 </DialogContent>
178 </Dialog>
179 );
180}
181
182/** The Apps icon at the foot of the rail's apps, and the launcher it opens beside it. */
183function AppsButton({ slug, pins, onToggle, current }: { slug: string; pins: PinnableApp[]; onToggle: (app: PinnableApp) => void; current: boolean }) {
184 const [open, setOpen] = useState(false);
185 return (
186 <Popover open={open} onOpenChange={setOpen}>
187 <SidebarMenuButton asChild size="icon" isActive={current} tooltip="Apps">
188 <PopoverTrigger aria-label="Apps">
189 <LayoutGrid />
190 </PopoverTrigger>
191 </SidebarMenuButton>
192 <PopoverContent side="right" align="end" sideOffset={12} aria-label="Apps" className="w-[23rem] p-2.5">
193 <AppsLauncher slug={slug} pins={pins} onToggle={onToggle} onClose={() => setOpen(false)} />
194 </PopoverContent>
195 </Popover>
196 );
197}
198
199/**
200 * The rail down the window's left edge: a narrow column of icons, each
201 * named by the hint beside it. g1t's mark, which leads to Home; the
The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404.202 * built-in apps (Home, Chat, Code and Artifacts) with what is
Notifications is the bell in the top bar alone: the rail no longer carries it, and the notifications guide points at the bell; a phone keeps it in the bottom bar, which has no bell. Home names who made a deployment, by username or by an agent's handle, instead of showing the account's id, and says someone when identity doesn't know the id. The Apps launcher's skeleton tiles are drawn in the line colour so they show on its raised surface.203 * unread on them (Notifications is the bell in the top bar, not a rail
204 * app); the apps you pinned; the Apps launcher;
The rail itself ships with the navigation it was committed without: the file that draws it was renamed from the dock and left out of the last change.205 * and at its foot People, Workspace and your account. The one you are in
206 * sits on a filled square, and which that is follows the address. A
207 * member without Code access has no Code. From 768px; a phone has the
208 * bottom bar (components/mobile.tsx). Drawn from the sidebar kit
209 * (components/ui/sidebar.tsx), as the mode sidebar beside it is.
210 */
211export function Rail({
212 workspace,
213 pins: saved,
214 unread,
215 account,
216}: {
217 workspace: Membership;
218 /** The apps pinned to this person's rail in this workspace, as saved. */
219 pins: PinnableApp[];
220 unread: { notifications: number; chat: number; mentions: number };
221 account: ReactNode;
222}) {
223 const { pathname } = useLocation();
224 const going = useNavigation().location?.pathname;
225 const path = going ?? pathname;
226 const here = modeOf(path, workspace.slug);
227 const slug = workspace.slug;
228 const code = hasCodeAccess(workspace);
229 const { pins, toggle } = useAppPins(slug, saved);
230 const builtins: Item[] = [
231 { key: "home", label: "Home" },
232 {
233 key: "chat",
234 label: "Chat",
235 badge: { count: unread.mentions > 0 ? unread.mentions : unread.chat, loud: unread.mentions > 0 },
236 },
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9237 ...(code ? [{ key: "code" as const, label: "Code" }] : []),
The rail itself ships with the navigation it was committed without: the file that draws it was renamed from the dock and left out of the last change.238 { key: "artifacts", label: "Artifacts" },
239 ];
240 const shown = pins.map((key) => pinnedApp(key, slug)).filter((app) => app != null);
241 // A pinned app is current on its own page and the pages under it.
242 const at = (to: string) => path === to || path.startsWith(`${to}/`);
243 return (
244 <Sidebar variant="rail" collapsible="none" role="navigation" aria-label="Apps">
245 <SidebarHeader className="h-14 items-center justify-center p-0">
246 <SidebarMenu className="w-auto">
247 <SidebarMenuItem>
248 <SidebarMenuButton asChild size="icon" tooltip="Home">
249 <Link to={homePagePath(slug)} aria-label="g1t: Home">
250 <span className="flex">
251 <Mark tight className="h-[1.125rem] w-auto" />
252 </span>
253 </Link>
254 </SidebarMenuButton>
255 </SidebarMenuItem>
256 </SidebarMenu>
257 </SidebarHeader>
258 <SidebarContent className="items-center gap-0">
259 <SidebarMenu className="w-auto shrink-0 gap-2">
260 {builtins.map((item) => (
261 <RailLink
262 key={item.key}
263 to={modeHome(item.key, slug)}
264 label={item.label}
265 icon={appIcon(item.key as BuiltinApp, 20)}
266 current={here === item.key && !shown.some((app) => at(app.to))}
267 badge={item.badge}
268 />
269 ))}
270 </SidebarMenu>
271 {/* Your pinned apps: as many as you like, scrolling under a fade at either end. */}
272 {shown.length > 0 && (
273 <SidebarMenu className="mt-2 w-auto min-h-0 grow gap-2 overflow-y-auto py-2 [mask-image:linear-gradient(to_bottom,transparent,#000_10px,#000_calc(100%-14px),transparent)] [scrollbar-width:none]">
274 {shown.map((app) => (
275 <RailLink key={app.key} to={app.to} label={app.name} icon={app.icon(20)} current={at(app.to)} />
276 ))}
277 </SidebarMenu>
278 )}
279 </SidebarContent>
280 <SidebarFooter className="items-center gap-0 p-0 pb-3">
281 <SidebarMenu className="w-auto gap-2">
282 <SidebarMenuItem>
283 <AppsButton slug={slug} pins={pins} onToggle={toggle} current={here === "apps"} />
284 </SidebarMenuItem>
285 </SidebarMenu>
286 <SidebarSeparator className="my-2.5 w-7" />
287 <SidebarMenu className="w-auto gap-2">
288 <RailLink to={modeHome("people", slug)} label="People" icon={appIcon("people", 20)} current={here === "people"} />
289 <RailLink to={modeHome("workspace", slug)} label="Workspace" icon={appIcon("workspace", 20)} current={here === "workspace"} />
290 <SidebarMenuItem className="mt-1 flex justify-center">{account}</SidebarMenuItem>
291 </SidebarMenu>
292 </SidebarFooter>
293 </Sidebar>
294 );
295}

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