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.
| 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. | 1 | import { |
| 2 | BarChart3, | |
| 3 | Bell, | |
| 4 | Boxes, | |
| 5 | Brain, | |
| 6 | Building2, | |
| 7 | Code2, | |
| 8 | History, | |
| 9 | LayoutGrid, | |
| 10 | MessagesSquare, | |
| 11 | Network, | |
| 12 | Package, | |
| 13 | Pin, | |
| 14 | Plug, | |
| 15 | Search, | |
| 16 | Shapes, | |
| 17 | ShieldCheck, | |
| 18 | Sparkles, | |
| 19 | Store, | |
| 20 | Sun, | |
| 21 | Users, | |
| 22 | UsersRound, | |
| 23 | Waypoints, | |
| 24 | } from "lucide-react"; | |
| 25 | import { type ReactNode, useMemo, useState } from "react"; | |
| 26 | import { Link, useFetcher, useFetchers } from "react-router"; | |
| 27 | ||
| 28 | import { type AppInfo, type AppKey, type PinnableApp, appPinFromForm, appsFor, withPin } from "../lib/apps"; | |
| 29 | import { cn } from "../lib/cn"; | |
| 30 | import { Hint } from "./ui/hint"; | |
| 31 | ||
| 32 | /** Each app's icon, at the size asked for. */ | |
| 33 | export function appIcon(key: AppKey, size = 18): ReactNode { | |
| 34 | const icons: Record<AppKey, ReactNode> = { | |
| 35 | today: <Sun size={size} />, | |
| 36 | chat: <MessagesSquare size={size} />, | |
| 37 | notifications: <Bell size={size} />, | |
| 38 | agents: <Sparkles size={size} />, | |
| 39 | code: <Code2 size={size} />, | |
| 40 | artifacts: <Shapes size={size} />, | |
| 41 | people: <Users size={size} />, | |
| 42 | workspace: <Building2 size={size} />, | |
| 43 | projects: <Boxes size={size} />, | |
| 44 | packages: <Package size={size} />, | |
| 45 | security: <ShieldCheck size={size} />, | |
| 46 | context: <Network size={size} />, | |
| 47 | memory: <Brain size={size} />, | |
| 48 | teams: <UsersRound size={size} />, | |
| 49 | usage: <BarChart3 size={size} />, | |
| 50 | gateway: <Waypoints size={size} />, | |
| 51 | integrations: <Plug size={size} />, | |
| 52 | audit: <History size={size} />, | |
| 53 | }; | |
| 54 | return icons[key]; | |
| 55 | } | |
| 56 | ||
| 57 | /** Every pin form shares this key, so the dock, the launcher and the Apps page see one change. */ | |
| 58 | const PIN_FETCHER = "app-pin"; | |
| 59 | ||
| 60 | /** | |
| 61 | * Someone's pins in the workspace `slug`, as saved (`saved`, from the | |
| 62 | * root's data), with any pin or unpin on its way already applied, so the | |
| 63 | * dock changes the moment a pin is pressed. `toggle` pins or unpins. | |
| 64 | */ | |
| 65 | export function useAppPins(slug: string, saved: PinnableApp[]) { | |
| 66 | const fetcher = useFetcher({ key: PIN_FETCHER }); | |
| 67 | const fetchers = useFetchers(); | |
| 68 | const pins = useMemo(() => { | |
| 69 | let pins = saved; | |
| 70 | for (const pending of fetchers) { | |
| 71 | if (pending.key !== PIN_FETCHER || !pending.formData) continue; | |
| 72 | const change = appPinFromForm(pending.formData); | |
| 73 | if (change) pins = withPin(pins, change.app, change.pinned); | |
| 74 | } | |
| 75 | return pins; | |
| 76 | }, [saved, fetchers]); | |
| 77 | const toggle = (app: PinnableApp) => { | |
| 78 | const form = new FormData(); | |
| 79 | form.set("intent", pins.includes(app) ? "unpin" : "pin"); | |
| 80 | form.set("app", app); | |
| 81 | fetcher.submit(form, { method: "post", action: `/${slug}/-/apps` }); | |
| 82 | }; | |
| 83 | return { pins, toggle }; | |
| 84 | } | |
| 85 | ||
| 86 | /** | |
| 87 | * One app: its icon and name, opening it; and, for an app people pin, a | |
| 88 | * pin that shows on hover or focus and stays lit while it is pinned. | |
| 89 | */ | |
| 90 | export function AppTile({ | |
| 91 | app, | |
| 92 | slug, | |
| 93 | pinned, | |
| 94 | onToggle, | |
| 95 | onOpen, | |
| 96 | card = false, | |
| 97 | }: { | |
| 98 | app: AppInfo; | |
| 99 | slug: string; | |
| 100 | pinned: boolean; | |
| 101 | onToggle: (app: PinnableApp) => void; | |
| 102 | onOpen?: () => void; | |
| 103 | /** On the Apps page: a card with what the app is. */ | |
| 104 | card?: boolean; | |
| 105 | }) { | |
| 106 | return ( | |
| 107 | <div | |
| 108 | className={cn( | |
| 109 | "group/tile relative rounded-xl transition-colors", | |
| 110 | card ? "border border-line bg-surface hover:border-line-strong" : "hover:bg-raised focus-within:bg-raised", | |
| 111 | )} | |
| 112 | > | |
| 113 | <Link | |
| 114 | to={app.path(slug)} | |
| 115 | prefetch="intent" | |
| 116 | onClick={onOpen} | |
| 117 | className={cn( | |
| 118 | "flex w-full rounded-xl text-center outline-none focus-visible:ring-2 focus-visible:ring-accent", | |
| 119 | card ? "items-center gap-3 p-3.5 pr-11 text-left" : "flex-col items-center gap-1.5 px-1 pt-3 pb-2.5", | |
| 120 | )} | |
| 121 | > | |
| 122 | <span className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-raised text-fg ring-1 ring-line group-hover/tile:ring-line-strong"> | |
| 123 | {appIcon(app.key, 19)} | |
| 124 | </span> | |
| 125 | {card ? ( | |
| 126 | <span className="min-w-0"> | |
| 127 | <span className="block truncate text-sm font-medium text-fg">{app.name}</span> | |
| 128 | <span className="block truncate text-xs text-muted">{app.about}</span> | |
| 129 | </span> | |
| 130 | ) : ( | |
| 131 | <span className="w-full truncate text-xs font-medium text-fg-soft">{app.name}</span> | |
| 132 | )} | |
| 133 | </Link> | |
| 134 | {!app.builtin && ( | |
| 135 | <Hint label={pinned ? "Unpin from your dock" : "Pin to your dock"}> | |
| 136 | <button | |
| 137 | type="button" | |
| 138 | aria-label={pinned ? `Unpin ${app.name} from your dock` : `Pin ${app.name} to your dock`} | |
| 139 | aria-pressed={pinned} | |
| 140 | onClick={() => onToggle(app.key as PinnableApp)} | |
| 141 | className={cn( | |
| 142 | "absolute flex size-6 items-center justify-center rounded-md outline-none transition-opacity hover:bg-line focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-accent [@media(hover:none)]:opacity-100", | |
| 143 | card ? "top-1/2 right-2.5 -translate-y-1/2" : "top-1 right-1", | |
| 144 | pinned ? "text-accent opacity-100" : "text-faint opacity-0 group-hover/tile:opacity-100 hover:text-fg", | |
| 145 | )} | |
| 146 | > | |
| 147 | <Pin size={13} className={pinned ? "fill-current" : undefined} /> | |
| 148 | </button> | |
| 149 | </Hint> | |
| 150 | )} | |
| 151 | </div> | |
| 152 | ); | |
| 153 | } | |
| 154 | ||
| 155 | /** | |
| 156 | * The Apps launcher's body: a search, every app in the workspace the | |
| 157 | * person can use, and the way to all of them and to the Marketplace. | |
| 158 | */ | |
| 159 | export function AppsLauncher({ | |
| 160 | slug, | |
| 161 | code, | |
| 162 | pins, | |
| 163 | onToggle, | |
| 164 | onClose, | |
| 165 | }: { | |
| 166 | slug: string; | |
| 167 | code: boolean; | |
| 168 | pins: PinnableApp[]; | |
| 169 | onToggle: (app: PinnableApp) => void; | |
| 170 | onClose: () => void; | |
| 171 | }) { | |
| 172 | const [query, setQuery] = useState(""); | |
| 173 | const wanted = query.trim().toLowerCase(); | |
| 174 | const apps = appsFor(code).filter((app) => !wanted || app.name.toLowerCase().includes(wanted) || app.about.toLowerCase().includes(wanted)); | |
| 175 | return ( | |
| 176 | <div className="flex flex-col gap-2"> | |
| 177 | <label className="flex h-9 items-center gap-2 rounded-lg border border-line bg-bg px-2.5 text-sm focus-within:border-line-strong"> | |
| 178 | <Search size={15} className="shrink-0 text-faint" /> | |
| 179 | <input | |
| 180 | value={query} | |
| 181 | onChange={(event) => setQuery(event.target.value)} | |
| 182 | placeholder="Find an app" | |
| 183 | aria-label="Find an app" | |
| 184 | autoComplete="off" | |
| 185 | className="min-w-0 grow bg-transparent outline-none placeholder:text-faint" | |
| 186 | /> | |
| 187 | </label> | |
| 188 | {apps.length === 0 ? ( | |
| 189 | <p className="px-2 py-6 text-center text-sm text-muted">No app matches.</p> | |
| 190 | ) : ( | |
| 191 | <div className="grid max-h-[min(23.5rem,60dvh)] grid-cols-4 gap-1 overflow-y-auto max-sm:grid-cols-3"> | |
| 192 | {apps.map((app) => ( | |
| 193 | <AppTile key={app.key} app={app} slug={slug} pinned={pins.includes(app.key as PinnableApp)} onToggle={onToggle} onOpen={onClose} /> | |
| 194 | ))} | |
| 195 | </div> | |
| 196 | )} | |
| 197 | <div className="flex items-center justify-between border-t border-line pt-2"> | |
| 198 | <Link | |
| 199 | to={`/${slug}/-/apps`} | |
| 200 | onClick={onClose} | |
| 201 | className="flex h-8 items-center gap-1.5 rounded-md px-2 text-[0.8125rem] text-muted transition-colors hover:bg-line hover:text-fg" | |
| 202 | > | |
| 203 | <LayoutGrid size={14} /> | |
| 204 | All apps | |
| 205 | </Link> | |
| 206 | <Hint label="There is no marketplace yet." disabled> | |
| 207 | <button type="button" disabled className="flex h-8 items-center gap-1.5 rounded-md px-2 text-[0.8125rem] text-faint"> | |
| 208 | <Store size={14} /> | |
| 209 | Marketplace | |
| 210 | <span className="rounded-full bg-line px-1.5 text-[0.625rem] font-medium text-muted">Coming</span> | |
| 211 | </button> | |
| 212 | </Hint> | |
| 213 | </div> | |
| 214 | </div> | |
| 215 | ); | |
| 216 | } |