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.
| Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell. | 1 | /** |
| 2 | * What Customize's pages do on the server besides reading: the connector | |
| 3 | * actions every page with a connector shares (signing in and out, a key, | |
| 4 | * listing tools again, where it reaches, removing it), and what follows | |
| 5 | * adding one: the browser goes to sign in, or to the connector's page. | |
| 6 | */ | |
| Merge branch 'main' into worktree-agent-a0d19db53ce9b29d8 | 7 | import { redirect } from "react-router"; |
| Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell. | 8 | |
| 9 | import type { ConnectorInput, User, WorkspaceConnector } from "@g1t/contracts"; | |
| 10 | ||
| 11 | import { addresses } from "./addresses.server"; | |
| Merge branch 'main' into worktree-agent-a0d19db53ce9b29d8 | 12 | import { connectorCallbackPath, connectorPagePath, splitTarget } from "./customize"; |
| Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell. | 13 | import { workspaceAgents } from "./services.server"; |
| 14 | import type { ActionResult } from "../components/agents/dialogs"; | |
| 15 | import { answer } from "../components/agents/actions.server"; | |
| 16 | ||
| 17 | /** Where a connector's sign-in comes back to: this g1t's own page. */ | |
| 18 | export function callbackUri(slug: string): string { | |
| 19 | return `${addresses().site}${connectorCallbackPath(slug)}`; | |
| 20 | } | |
| 21 | ||
| 22 | /** The browser leaves for the connector's sign-in page, or lands on the connector's own page with what happened. */ | |
| 23 | async function toSignIn(slug: string, viewer: User, id: string): Promise<Response> { | |
| 24 | const started = await workspaceAgents.startConnectorSignIn(slug, viewer, id, callbackUri(slug)).catch(() => null); | |
| 25 | if (started?.ok) return redirect(started.value.url); | |
| 26 | const problem = started ? started.error.message : "The agents service didn't answer. Try again in a moment."; | |
| 27 | return redirect(`${connectorPagePath(slug, id)}?problem=${encodeURIComponent(problem)}`); | |
| 28 | } | |
| 29 | ||
| 30 | /** | |
| 31 | * Adds a connector and takes the browser where it goes next: to sign in | |
| 32 | * when the server asks for that, or to the connector's page otherwise. An | |
| 33 | * answer (not a redirect) says why it couldn't be added. | |
| 34 | */ | |
| 35 | export async function addConnectorThen(slug: string, viewer: User, input: ConnectorInput): Promise<Response | ActionResult> { | |
| 36 | const added = await workspaceAgents.addConnector(slug, viewer, input).catch(() => null); | |
| 37 | if (!added) return { ok: false, intent: "connect", error: "The agents service didn't answer. Try again in a moment." }; | |
| 38 | if (!added.ok) return { ok: false, intent: "connect", error: added.error.message, code: added.error.code }; | |
| 39 | return afterAdd(slug, viewer, added.value); | |
| 40 | } | |
| 41 | ||
| 42 | /** After a connector is added: the sign-in when it needs one, its page otherwise. */ | |
| 43 | export function afterAdd(slug: string, viewer: User, connector: WorkspaceConnector): Promise<Response> | Response { | |
| 44 | if (connector.state === "needs_sign_in") return toSignIn(slug, viewer, connector.id); | |
| 45 | return redirect(`${connectorPagePath(slug, connector.id)}?added=1`); | |
| 46 | } | |
| 47 | ||
| 48 | /** | |
| 49 | * The actions a connector has wherever it is shown. `intent` names one; | |
| 50 | * null says it isn't one of them, so the page's own action goes on. | |
| 51 | * `afterRemove` is where the browser goes once it is removed. | |
| 52 | */ | |
| 53 | export async function connectorAction(intent: string, form: FormData, slug: string, viewer: User, afterRemove: string | null): Promise<Response | ActionResult | null> { | |
| 54 | const id = String(form.get("connector") ?? ""); | |
| 55 | switch (intent) { | |
| 56 | case "sign_in": | |
| 57 | return toSignIn(slug, viewer, id); | |
| 58 | case "sign_out": | |
| 59 | return answer(intent, workspaceAgents.signOutConnector(slug, viewer, id)); | |
| 60 | case "key": | |
| 61 | return answer(intent, workspaceAgents.setConnectorKey(slug, viewer, id, String(form.get("header_name") ?? "") || null, String(form.get("value") ?? ""))); | |
| 62 | case "refresh": | |
| 63 | return answer(intent, workspaceAgents.refreshConnector(slug, viewer, id)); | |
| 64 | case "grant": { | |
| 65 | const { scope, target } = splitTarget(String(form.get("where") ?? "")); | |
| 66 | return answer(intent, workspaceAgents.grantConnector(slug, viewer, id, scope, target)); | |
| 67 | } | |
| 68 | case "revoke": | |
| 69 | return answer(intent, workspaceAgents.revokeConnector(slug, viewer, id, String(form.get("grant") ?? ""))); | |
| 70 | case "remove": { | |
| 71 | const result = await answer(intent, workspaceAgents.removeConnector(slug, viewer, id)); | |
| 72 | return result.ok && afterRemove ? redirect(afterRemove) : result; | |
| 73 | } | |
| 74 | default: | |
| 75 | return null; | |
| 76 | } | |
| 77 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.