Skip to content
106 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.1/**
2 * Apps: every app in the workspace that you can use, the ones pinned to
3 * your dock first. Built-in apps are always in the dock; the rest you pin
4 * or unpin here, from the launcher, or on a phone from More. Pins are
5 * yours alone, per workspace, kept in a cookie (lib/apps.ts) so the dock
6 * is drawn right from the server. The pin buttons post here.
7 */
8import { Store } from "lucide-react";
9import { data } from "react-router";
10
11import { hasCodeAccess } from "@g1t/contracts";
12
13import type { Route } from "./+types/apps";
14import { AppTile, useAppPins } from "../../components/apps";
15import { DOCK_COOKIE, type PinnableApp, appPinFromForm, appsFor, dockCookie, pinsIn, withPin, writeDock } from "../../lib/apps";
16import { page } from "../../lib/meta";
17import { readCookie } from "../../lib/mission";
18import { assertSameOrigin, getViewer, requireUser } from "../../lib/session.server";
19
20export function meta({ params, ...args }: Route.MetaArgs) {
21 return page(args, { title: `Apps · ${params.owner} · g1t` });
22}
23
24export function loader({ params, context, request }: Route.LoaderArgs) {
25 const viewer = getViewer(context);
26 const slug = params.owner.toLowerCase();
27 const membership = viewer?.workspaces?.find((m) => m.slug === slug);
28 if (!membership) throw data(null, { status: 404 });
29 return {
30 slug,
31 name: membership.name?.trim() || slug,
32 code: hasCodeAccess(membership),
33 pins: pinsIn(readCookie(request.headers.get("cookie"), DOCK_COOKIE), slug),
34 };
35}
36
37export async function action({ params, context, request }: Route.ActionArgs) {
38 assertSameOrigin(request);
39 const user = requireUser(context, request);
40 const slug = params.owner.toLowerCase();
41 if (!user.workspaces?.some((m) => m.slug === slug)) throw data(null, { status: 404 });
42 const change = appPinFromForm(await request.formData());
43 if (!change) return data({ error: "Nothing to do." }, { status: 400 });
44 const saved = readCookie(request.headers.get("cookie"), DOCK_COOKIE);
45 const pins = withPin(pinsIn(saved, slug), change.app, change.pinned);
46 const secure = new URL(request.url).protocol === "https:";
47 return data({ error: null, pins }, { headers: { "Set-Cookie": dockCookie(writeDock(saved, slug, pins), secure) } });
48}
49
50export default function Apps({ loaderData }: Route.ComponentProps) {
51 const { slug, name, code } = loaderData;
52 const { pins, toggle } = useAppPins(slug, loaderData.pins);
53 const apps = appsFor(code);
54 const pinned = pins.map((key) => apps.find((app) => app.key === key)).filter((app) => app != null);
55 return (
56 <main className="mx-auto max-w-5xl px-4 py-8 sm:px-8 sm:py-10">
57 <div className="flex flex-wrap items-end justify-between gap-4">
58 <div>
59 <h1 className="text-2xl font-semibold tracking-tight">Apps</h1>
60 <p className="mt-1 max-w-xl text-sm text-muted">
61 Everything in {name} that you can use. Pin the ones you want in your dock: your dock is yours, and nobody else sees your pins.
62 </p>
63 </div>
64 <span className="inline-flex h-9 items-center gap-2 rounded-md border border-line px-3 text-sm text-faint">
65 <Store size={15} />
66 Marketplace
67 <span className="rounded-full bg-line px-1.5 text-[0.6875rem] font-medium text-muted">Coming</span>
68 </span>
69 </div>
70
71 <section aria-labelledby="pinned" className="mt-8">
72 <div className="flex items-baseline justify-between gap-3">
73 <h2 id="pinned" className="text-sm font-semibold">
74 Pinned to your dock
75 </h2>
76 <span className="text-xs text-faint">{pinned.length === 0 ? "None yet" : `${pinned.length} pinned`}</span>
77 </div>
78 {pinned.length === 0 ? (
79 <p className="mt-3 rounded-xl border border-dashed border-line px-4 py-6 text-center text-sm text-muted">
80 Nothing pinned yet. Pin an app below and it shows in your dock, under the built-in ones.
81 </p>
82 ) : (
83 <div className="mt-3 grid grid-cols-[repeat(auto-fill,minmax(15rem,1fr))] gap-2">
84 {pinned.map((app) => (
85 <AppTile key={app.key} app={app} slug={slug} pinned onToggle={toggle} card />
86 ))}
87 </div>
88 )}
89 </section>
90
91 <section aria-labelledby="yours" className="mt-10">
92 <div className="flex items-baseline justify-between gap-3">
93 <h2 id="yours" className="text-sm font-semibold">
94 Your apps
95 </h2>
96 <span className="text-xs text-faint">Built-in apps are always in your dock</span>
97 </div>
98 <div className="mt-3 grid grid-cols-[repeat(auto-fill,minmax(15rem,1fr))] gap-2">
99 {apps.map((app) => (
100 <AppTile key={app.key} app={app} slug={slug} pinned={pins.includes(app.key as PinnableApp)} onToggle={toggle} card />
101 ))}
102 </div>
103 </section>
104 </main>
105 );
106}