Skip to content
216 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.

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.1import {
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";
25import { type ReactNode, useMemo, useState } from "react";
26import { Link, useFetcher, useFetchers } from "react-router";
27
28import { type AppInfo, type AppKey, type PinnableApp, appPinFromForm, appsFor, withPin } from "../lib/apps";
29import { cn } from "../lib/cn";
30import { Hint } from "./ui/hint";
31
32/** Each app's icon, at the size asked for. */
33export 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. */
58const 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 */
65export 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 */
90export 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 */
159export 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}