Skip to content
86 linesCodeBlameRaw
1/**
2 * Links into the app. A `g1t://` link names a site path, as on the desktop
3 * (apps/desktop/src-tauri/src/links.rs): `g1t://acme/-/chat/dm/chn_1` is
4 * the page `/acme/-/chat/dm/chn_1`. The app has screens for some of the
5 * site's pages; `routeFor` says which screen shows a site path, and the
6 * rest open on the site.
7 *
8 * Kept free of React Native imports so `npm test` runs it under Node.
9 */
10
11/** Where the app's OAuth sign-in comes back to. */
12export const OAUTH_CALLBACK_PATH = "/oauth/callback";
13
14/** Customize's tabs, each a screen of the app's Customize. */
15const CUSTOMIZE_TABS = new Set(["skills", "connectors", "plugins"]);
16
17/** Path segments a workspace or account can never be named (the site's own pages). */
18const NOT_OWNERS = new Set(["settings", "notifications", "oauth", "device", "login", "register", "-"]);
19
20/**
21 * The app's screen for a site path, or null when the app has none and the
22 * page should open on the site.
23 */
24export function routeFor(sitePath: string): string | null {
25 const [pathname, query = ""] = sitePath.split("?", 2);
26 const parts = pathname.split("/").filter(Boolean).map(decodeURIComponentSafe);
27 const keep = query ? `?${query}` : "";
28 if (parts.length === 0) return "/";
29 if (`/${parts.join("/")}` === OAUTH_CALLBACK_PATH) return OAUTH_CALLBACK_PATH + keep;
30 const [owner, second, third, fourth, fifth] = parts;
31 // `g1t://connect?server=<site>`: point the app at a g1t, as a self-hosted one's download page offers.
32 if (owner === "connect" && !second && /(^|&)server=/.test(query)) return `/connect?${query}`;
33 if (NOT_OWNERS.has(owner.toLowerCase())) return null;
34 const workspace = `workspace=${encodeURIComponent(owner)}`;
35
36 // A workspace's own pages: /<workspace>/-/<page>.
37 if (second === "-") {
38 if (third === "chat") {
39 if (!fourth) return `/chat?${workspace}`;
40 if (fourth === "dm" && fifth) return `/chat/${encodeURIComponent(fifth)}?${workspace}`;
41 if (fourth !== "browse" && fourth !== "dm" && !fifth) return `/chat/${encodeURIComponent(fourth)}?${workspace}`;
42 return null;
43 }
44 if (third === "overview" && !fourth) return `/code?${workspace}`;
45 // Customize's tabs open the app's Customize, on the same tab; its other
46 // pages (a plugin, a connector, requests) open on the site. The
47 // Marketplace, the integrations settings and the old skill library were
48 // Customize before it had the name.
49 if (third === "customize") {
50 if (!fourth) return `/customize?${workspace}`;
51 if (CUSTOMIZE_TABS.has(fourth) && !fifth) return `/customize/${fourth}?${workspace}`;
52 return null;
53 }
54 if (third === "marketplace" || third === "integrations" || (third === "agents" && fourth === "skills" && !fifth)) {
55 return `/customize?${workspace}`;
56 }
57 return null;
58 }
59
60 // A repository's pages: /<owner>/<repo>[/pull/<n> | /issues/<n>].
61 if (!second) return null;
62 const repo = `/code/${encodeURIComponent(owner)}/${encodeURIComponent(second)}`;
63 if (!third) return repo;
64 if (third === "pull" && fourth && /^\d+$/.test(fourth) && !fifth) return `${repo}/pull/${fourth}`;
65 if (third === "issues" && fourth && /^\d+$/.test(fourth) && !fifth) return `${repo}/issues/${fourth}`;
66 return null;
67}
68
69/**
70 * The site path a `g1t://` link names, or null for any other link.
71 * `g1t://acme/-/chat?x` is `/acme/-/chat?x`.
72 */
73export function sitePathOf(link: string): string | null {
74 const match = /^g1t:\/\/([^?#]*)(\?[^#]*)?/i.exec(link);
75 if (!match) return null;
76 const path = `/${match[1].replace(/^\/+/, "")}`;
77 return path + (match[2] ?? "");
78}
79
80function decodeURIComponentSafe(part: string): string {
81 try {
82 return decodeURIComponent(part);
83 } catch {
84 return part;
85 }
86}