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.
| Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules | 1 | import { identity, repos } from "../../lib/services.server"; |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 2 | import { Form, Link, redirect, useSearchParams } from "react-router"; |
| 3 | ||
| 4 | import { presetScopes, type AccessToken } from "@g1t/contracts"; | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 5 | |
| 6 | import type { Route } from "./+types/tokens"; | |
| 7 | import { page } from "../../lib/meta"; | |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 8 | import { |
| Merge branch 'worktree-agent-a985c01f43538af01' | 9 | SubmitButton, |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 10 | ButtonLink, |
| 11 | ErrorText, | |
| 12 | Field, | |
| 13 | Input, | |
| 14 | TimeAgo, | |
| 15 | } from "../../components/ui"; | |
| Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules | 16 | import { Badge } from "../../components/ui/badge"; |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 17 | import { DeleteButton } from "../../components/account-settings"; |
| Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules | 18 | import { FineGrainedForm, type OwnerChoice } from "../../components/fine-grained-form"; |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 19 | import { |
| 20 | AccessSummary, | |
| 21 | ExpiryField, | |
| 22 | ScopeChecklist, | |
| 23 | } from "../../components/token-scopes"; | |
| 24 | import { | |
| 25 | describeExpiry, | |
| 26 | expiryTtl, | |
| 27 | grantFromForm, | |
| 28 | } from "../../lib/token-scopes"; | |
| Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules | 29 | import { fineGrainedFromForm, permissionChips, reachSummary, statusBadge } from "../../lib/fine-grained"; |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 30 | import { assertSameOrigin, requireUser } from "../../lib/session.server"; |
| Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules | 31 | import { cn } from "../../lib/cn"; |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 32 | |
| 33 | export function meta(args: Route.MetaArgs) { | |
| 34 | return page(args, { title: "Access tokens · Settings · g1t" }); | |
| 35 | } | |
| 36 | ||
| Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules | 37 | type Tab = "fine-grained" | "classic"; |
| 38 | ||
| 39 | function tabOf(value: string | null): Tab { | |
| 40 | return value === "classic" ? "classic" : "fine-grained"; | |
| 41 | } | |
| 42 | ||
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 43 | export async function loader({ request, context }: Route.LoaderArgs) { |
| 44 | const user = requireUser(context, request); | |
| Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules | 45 | const memberships = user.workspaces ?? []; |
| 46 | // Each workspace you belong to, with its rules for tokens and the | |
| 47 | // repositories you can choose from. | |
| 48 | const [tokens, owners] = await Promise.all([ | |
| 49 | identity.listAccessTokens(user), | |
| 50 | Promise.all( | |
| 51 | memberships.map(async (membership): Promise<OwnerChoice> => { | |
| 52 | const [policy, listed] = await Promise.all([ | |
| 53 | identity.getTokenPolicy(membership.slug, user), | |
| 54 | repos.list(user, { namespace: membership.slug }).catch(() => []), | |
| 55 | ]); | |
| 56 | return { | |
| 57 | slug: membership.slug, | |
| 58 | owner: membership.role === "owner", | |
| 59 | policy: policy.ok ? policy.value : null, | |
| 60 | repos: listed.map((repo) => `${repo.namespace}/${repo.name}`).sort(), | |
| 61 | }; | |
| 62 | }), | |
| 63 | ), | |
| 64 | ]); | |
| 65 | return { user, tokens, owners }; | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 66 | } |
| 67 | ||
| Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules | 68 | type ActionResult = { |
| 69 | newToken: string | null; | |
| 70 | created: AccessToken | null; | |
| 71 | error: string | null; | |
| 72 | editing: string | null; | |
| 73 | }; | |
| 74 | ||
| 75 | const failed = (error: string, editing: string | null = null): ActionResult => ({ newToken: null, created: null, error, editing }); | |
| 76 | ||
| 77 | export async function action({ request, context }: Route.ActionArgs): Promise<ActionResult | null> { | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 78 | assertSameOrigin(request); |
| 79 | const user = requireUser(context, request); | |
| 80 | const form = await request.formData(); | |
| 81 | switch (form.get("intent")) { | |
| 82 | case "add-token": { | |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 83 | const grant = grantFromForm(form); |
| Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules | 84 | if (!grant.ok) return failed(grant.error); |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 85 | const created = await identity.createAccessToken( |
| 86 | user, | |
| 87 | String(form.get("label") ?? ""), | |
| 88 | expiryTtl(form.get("expires")), | |
| 89 | { ...grant.value, listed: true }, | |
| 90 | ); | |
| Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules | 91 | return { newToken: created.token, created: created.info, error: null, editing: null }; |
| 92 | } | |
| 93 | case "add-fine-grained": { | |
| 94 | const input = fineGrainedFromForm(form); | |
| 95 | if (!input.ok) return failed(input.error); | |
| 96 | const created = await identity.createFineGrainedToken(user, input.value); | |
| 97 | if (!created.ok) return failed(created.error.message); | |
| 98 | return { newToken: created.value.token, created: created.value.info, error: null, editing: null }; | |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 99 | } |
| 100 | case "update-token": { | |
| 101 | const id = String(form.get("id") ?? ""); | |
| 102 | const grant = grantFromForm(form); | |
| Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules | 103 | if (!grant.ok) return failed(grant.error, id); |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 104 | const updated = await identity.updateAccessToken(user, id, grant.value); |
| Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules | 105 | if (!updated.ok) return failed(updated.error.message, id); |
| 106 | throw redirect("/settings/tokens?tab=classic"); | |
| 107 | } | |
| 108 | case "update-fine-grained": { | |
| 109 | const id = String(form.get("id") ?? ""); | |
| 110 | const input = fineGrainedFromForm(form, { editing: true }); | |
| 111 | if (!input.ok) return failed(input.error, id); | |
| 112 | const { name, description, repositorySelection, repositories, permissions } = input.value; | |
| 113 | const updated = await identity.updateFineGrainedToken(user, id, { | |
| 114 | name: name || undefined, | |
| 115 | description: description ?? "", | |
| 116 | repositorySelection, | |
| 117 | repositories, | |
| 118 | permissions, | |
| 119 | }); | |
| 120 | if (!updated.ok) return failed(updated.error.message, id); | |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 121 | throw redirect("/settings/tokens"); |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 122 | } |
| 123 | case "delete-token": | |
| 124 | await identity.removeAccessToken(user, String(form.get("id") ?? "")); | |
| 125 | return null; | |
| 126 | } | |
| 127 | return null; | |
| 128 | } | |
| 129 | ||
| Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules | 130 | export default function TokenSettings({ loaderData, actionData }: Route.ComponentProps) { |
| 131 | const { user, tokens, owners } = loaderData; | |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 132 | const [params] = useSearchParams(); |
| Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules | 133 | const created = actionData?.created ?? null; |
| 134 | const tab: Tab = created ? (created.kind === "fine_grained" ? "fine-grained" : "classic") : tabOf(params.get("tab")); | |
| 135 | const fine = tokens.filter((token) => token.kind === "fine_grained"); | |
| 136 | const classic = tokens.filter((token) => token.kind !== "fine_grained"); | |
| 137 | const workspaces = (user.workspaces ?? []).map((membership) => membership.slug); | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 138 | return ( |
| 139 | <section id="tokens" className="scroll-mt-20"> | |
| Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules | 140 | <nav aria-label="Kinds of token" className="mb-5 flex gap-1 border-b border-line"> |
| 141 | {( | |
| 142 | [ | |
| 143 | ["fine-grained", "Fine-grained tokens", fine.length], | |
| 144 | ["classic", "Tokens (classic)", classic.length], | |
| 145 | ] as const | |
| 146 | ).map(([id, label, count]) => ( | |
| 147 | <Link | |
| 148 | key={id} | |
| 149 | to={id === "classic" ? "?tab=classic" : "?"} | |
| 150 | preventScrollReset | |
| 151 | aria-current={tab === id ? "page" : undefined} | |
| 152 | className={cn( | |
| 153 | "-mb-px flex items-center gap-2 border-b-2 px-3 pb-2.5 text-sm whitespace-nowrap transition-colors", | |
| 154 | tab === id ? "border-accent font-medium text-fg" : "border-transparent text-muted hover:text-fg", | |
| 155 | )} | |
| 156 | > | |
| 157 | {label} | |
| 158 | {count > 0 && <span className="rounded-full bg-raised px-1.5 py-px text-xs text-muted">{count}</span>} | |
| 159 | </Link> | |
| 160 | ))} | |
| 161 | </nav> | |
| 162 | ||
| 163 | {actionData?.newToken && created && ( | |
| 164 | <div className="mb-5 rounded-md border border-accent/40 bg-surface p-4"> | |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 165 | <p className="text-sm"> |
| Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules | 166 | <span className="font-medium">{created.name}</span> is ready. Copy it now. It will not be shown again. |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 167 | </p> |
| Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules | 168 | <pre className="mt-2 font-mono text-sm break-all whitespace-pre-wrap text-accent">{actionData.newToken}</pre> |
| 169 | {created.kind === "fine_grained" ? <FineGrainedSummary token={created} /> : <AccessSummary holder={created} className="mt-3" />} | |
| 170 | <p className="mt-1.5 text-xs text-faint">{describeExpiry(created.expiresAt)}</p> | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 171 | </div> |
| 172 | )} | |
| Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules | 173 | |
| 174 | {tab === "fine-grained" ? ( | |
| 175 | <FineGrainedTokens tokens={fine} owners={owners} actionData={actionData ?? null} /> | |
| 176 | ) : ( | |
| 177 | <ClassicTokens tokens={classic} workspaces={workspaces} actionData={actionData ?? null} /> | |
| 178 | )} | |
| 179 | </section> | |
| 180 | ); | |
| 181 | } | |
| 182 | ||
| 183 | /** A fine-grained token's reach, permissions and status, under its name. */ | |
| 184 | function FineGrainedSummary({ token }: { token: AccessToken }) { | |
| 185 | const badge = statusBadge(token.fineGrained?.status); | |
| 186 | return ( | |
| 187 | <div className="mt-1.5 space-y-1.5"> | |
| 188 | <p className="flex flex-wrap items-center gap-1.5 text-xs text-muted"> | |
| 189 | <Badge tone="accent">Fine-grained</Badge> | |
| 190 | {badge && <Badge tone={badge.tone}>{badge.label}</Badge>} | |
| 191 | <span>{reachSummary(token)}</span> | |
| 192 | </p> | |
| 193 | <div className="flex flex-wrap gap-1.5"> | |
| 194 | {permissionChips(token.fineGrained?.permissions).map((chip) => ( | |
| 195 | <span key={chip} className="rounded border border-line px-1.5 py-px text-[0.6875rem] text-muted"> | |
| 196 | {chip} | |
| 197 | </span> | |
| 198 | ))} | |
| 199 | </div> | |
| 200 | {token.fineGrained?.repositorySelection === "selected" && token.fineGrained.repositories.length > 0 && ( | |
| 201 | <p className="truncate font-mono text-[0.6875rem] text-faint">{token.fineGrained.repositories.join(", ")}</p> | |
| 202 | )} | |
| 203 | {token.fineGrained?.reviewReason && ( | |
| 204 | <p className="text-xs text-faint">Owner's note: {token.fineGrained.reviewReason}</p> | |
| 205 | )} | |
| 206 | </div> | |
| 207 | ); | |
| 208 | } | |
| 209 | ||
| 210 | function Meta({ token }: { token: AccessToken }) { | |
| 211 | const expiry = describeExpiry(token.expiresAt); | |
| 212 | return ( | |
| 213 | <p className="text-xs text-faint"> | |
| 214 | Created <TimeAgo at={token.createdAt} /> ·{" "} | |
| 215 | {token.lastUsedAt ? ( | |
| 216 | <> | |
| 217 | last used <TimeAgo at={token.lastUsedAt} /> | |
| 218 | </> | |
| 219 | ) : ( | |
| 220 | "never used" | |
| 221 | )}{" "} | |
| 222 | · <span className={expiry === "Expired" ? "text-danger" : undefined}>{expiry}</span> | |
| 223 | </p> | |
| 224 | ); | |
| 225 | } | |
| 226 | ||
| 227 | function FineGrainedTokens({ | |
| 228 | tokens, | |
| 229 | owners, | |
| 230 | actionData, | |
| 231 | }: { | |
| 232 | tokens: AccessToken[]; | |
| 233 | owners: OwnerChoice[]; | |
| 234 | actionData: ActionResult | null; | |
| 235 | }) { | |
| 236 | const [params] = useSearchParams(); | |
| 237 | const editing = actionData?.editing ?? params.get("edit"); | |
| 238 | const created = actionData?.created; | |
| 239 | return ( | |
| 240 | <> | |
| 241 | <p className="text-sm text-muted"> | |
| 242 | A fine-grained token reaches one resource owner (one workspace, or your own account), only the repositories | |
| 243 | you choose, and only with the permissions you give it. It always expires. | |
| 244 | </p> | |
| 245 | <ul className="mt-4 divide-y divide-line rounded-md border border-line empty:hidden"> | |
| 246 | {tokens.map((token) => ( | |
| 247 | <li key={token.id} className="px-4 py-3"> | |
| 248 | <div className="flex flex-wrap items-start gap-x-4 gap-y-2"> | |
| 249 | <div className="min-w-0 grow basis-60"> | |
| 250 | <p className="truncate text-sm font-medium">{token.name}</p> | |
| 251 | {token.description && <p className="truncate text-xs text-muted">{token.description}</p>} | |
| 252 | <Meta token={token} /> | |
| 253 | <FineGrainedSummary token={token} /> | |
| 254 | </div> | |
| 255 | <div className="ml-auto flex shrink-0 items-center gap-2"> | |
| 256 | {editing !== token.id && ( | |
| 257 | <ButtonLink variant="quiet" to={`?edit=${token.id}`} preventScrollReset> | |
| 258 | Edit | |
| 259 | </ButtonLink> | |
| 260 | )} | |
| 261 | <DeleteButton intent="delete-token" id={token.id} /> | |
| 262 | </div> | |
| 263 | </div> | |
| 264 | {editing === token.id && ( | |
| 265 | <Form method="post" className="mt-4 space-y-5 border-t border-line pt-4"> | |
| 266 | <input type="hidden" name="intent" value="update-fine-grained" /> | |
| 267 | <input type="hidden" name="id" value={token.id} /> | |
| 268 | <p className="text-sm text-muted"> | |
| 269 | The token stays the same; what it reaches changes from its next request. A workspace that approves | |
| 270 | tokens asks again when you widen it. | |
| 271 | </p> | |
| 272 | <FineGrainedForm owners={owners} editing={token} /> | |
| 273 | <ErrorText>{actionData?.editing === token.id ? actionData.error : null}</ErrorText> | |
| 274 | <div className="flex gap-2"> | |
| 275 | <SubmitButton pending="Saving…" match={{ intent: "update-fine-grained", id: token.id }}> | |
| 276 | Save | |
| 277 | </SubmitButton> | |
| 278 | <ButtonLink variant="quiet" to="." preventScrollReset> | |
| 279 | Cancel | |
| 280 | </ButtonLink> | |
| 281 | </div> | |
| 282 | </Form> | |
| 283 | )} | |
| 284 | </li> | |
| 285 | ))} | |
| 286 | </ul> | |
| 287 | {!editing && ( | |
| 288 | <Form key={created?.id ?? "new"} method="post" className="mt-8 space-y-5 rounded-md border border-line p-4 sm:p-5"> | |
| 289 | <input type="hidden" name="intent" value="add-fine-grained" /> | |
| 290 | <h2 className="font-medium">New fine-grained token</h2> | |
| 291 | <FineGrainedForm owners={owners} /> | |
| 292 | {!actionData?.editing && <ErrorText>{actionData?.error}</ErrorText>} | |
| 293 | <SubmitButton pending="Generating…" match={{ intent: "add-fine-grained" }}> | |
| 294 | Generate token | |
| 295 | </SubmitButton> | |
| 296 | </Form> | |
| 297 | )} | |
| 298 | </> | |
| 299 | ); | |
| 300 | } | |
| 301 | ||
| 302 | function ClassicTokens({ | |
| 303 | tokens, | |
| 304 | workspaces, | |
| 305 | actionData, | |
| 306 | }: { | |
| 307 | tokens: AccessToken[]; | |
| 308 | workspaces: string[]; | |
| 309 | actionData: ActionResult | null; | |
| 310 | }) { | |
| 311 | const [params] = useSearchParams(); | |
| 312 | const editing = actionData?.editing ?? params.get("edit"); | |
| 313 | const created = actionData?.created; | |
| 314 | return ( | |
| 315 | <> | |
| 316 | <p className="text-sm text-muted"> | |
| 317 | A classic token reaches every workspace and repository you can, and its scopes say what it may do there. A | |
| 318 | workspace can keep classic tokens out. | |
| 319 | {workspaces.length > 0 && ( | |
| 320 | <> | |
| 321 | {" "} | |
| 322 | For CI and integrations that work for a team, use a workspace's own tokens instead:{" "} | |
| 323 | {workspaces.map((slug, i) => ( | |
| 324 | <span key={slug}> | |
| 325 | {i > 0 && ", "} | |
| 326 | <Link to={`/${slug}/-/tokens`} className="font-mono text-fg underline underline-offset-4"> | |
| 327 | {slug} | |
| 328 | </Link> | |
| 329 | </span> | |
| 330 | ))} | |
| 331 | . | |
| 332 | </> | |
| 333 | )} | |
| 334 | </p> | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 335 | <ul className="mt-4 divide-y divide-line rounded-md border border-line empty:hidden"> |
| 336 | {tokens.map((token) => ( | |
| Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules | 337 | <ClassicRow |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 338 | key={token.id} |
| 339 | token={token} | |
| 340 | editing={editing === token.id} | |
| 341 | error={actionData?.editing === token.id ? actionData.error : null} | |
| 342 | /> | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 343 | ))} |
| 344 | </ul> | |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 345 | |
| 346 | {!editing && ( | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 347 | // Keyed on the token just made, so the fields start over for the next. |
| Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules | 348 | <Form key={created?.id ?? "new"} method="post" className="mt-8 space-y-5 rounded-md border border-line p-4 sm:p-5"> |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 349 | <input type="hidden" name="intent" value="add-token" /> |
| Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules | 350 | <h2 className="font-medium">New classic token</h2> |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 351 | <div className="grid gap-4 sm:grid-cols-[1fr_11rem]"> |
| 352 | <Field label="Name" hint="Name it after what will use it."> | |
| Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules | 353 | <Input name="label" maxLength={100} placeholder="laptop" required /> |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 354 | </Field> |
| 355 | <ExpiryField /> | |
| 356 | </div> | |
| 357 | <ScopeChecklist initial={presetScopes("agent")} /> | |
| 358 | {!actionData?.editing && <ErrorText>{actionData?.error}</ErrorText>} | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 359 | <SubmitButton pending="Creating…" match={{ intent: "add-token" }}> |
| 360 | Create token | |
| 361 | </SubmitButton> | |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 362 | </Form> |
| 363 | )} | |
| Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules | 364 | </> |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 365 | ); |
| 366 | } | |
| 367 | ||
| Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules | 368 | function ClassicRow({ |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 369 | token, |
| 370 | editing, | |
| 371 | error, | |
| 372 | }: { | |
| 373 | token: AccessToken; | |
| 374 | editing: boolean; | |
| 375 | error: string | null | undefined; | |
| 376 | }) { | |
| 377 | const legacy = token.legacy && token.scopes === null; | |
| 378 | return ( | |
| 379 | <li className="px-4 py-3"> | |
| 380 | <div className="flex flex-wrap items-start gap-x-4 gap-y-2"> | |
| 381 | <div className="min-w-0 grow basis-60"> | |
| 382 | <p className="truncate text-sm font-medium">{token.name}</p> | |
| Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules | 383 | <Meta token={token} /> |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 384 | <AccessSummary holder={token} /> |
| 385 | {legacy && ( | |
| 386 | <p className="mt-1.5 text-xs text-warn"> | |
| 387 | Made before tokens had scopes, so it can do everything you can. | |
| 388 | Narrow it to what it needs. | |
| 389 | </p> | |
| 390 | )} | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 391 | </div> |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 392 | <div className="ml-auto flex shrink-0 items-center gap-2"> |
| 393 | {!editing && ( | |
| 394 | <ButtonLink | |
| 395 | variant="quiet" | |
| Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules | 396 | to={`?tab=classic&edit=${token.id}`} |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 397 | preventScrollReset |
| 398 | > | |
| 399 | {legacy ? "Narrow this token" : "Edit access"} | |
| 400 | </ButtonLink> | |
| 401 | )} | |
| 402 | <DeleteButton intent="delete-token" id={token.id} /> | |
| 403 | </div> | |
| 404 | </div> | |
| 405 | {editing && ( | |
| 406 | <Form | |
| 407 | method="post" | |
| 408 | className="mt-4 space-y-5 border-t border-line pt-4" | |
| 409 | > | |
| 410 | <input type="hidden" name="intent" value="update-token" /> | |
| 411 | <input type="hidden" name="id" value={token.id} /> | |
| 412 | <p className="text-sm text-muted"> | |
| 413 | The token stays the same; only what it may do changes, from its next | |
| 414 | request. | |
| 415 | </p> | |
| 416 | <ScopeChecklist initial={token.scopes} /> | |
| 417 | <ErrorText>{error}</ErrorText> | |
| 418 | <div className="flex gap-2"> | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 419 | <SubmitButton pending="Saving…" match={{ intent: "update-token", id: token.id }}> |
| 420 | Save access | |
| 421 | </SubmitButton> | |
| Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules | 422 | <ButtonLink variant="quiet" to="?tab=classic" preventScrollReset> |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 423 | Cancel |
| 424 | </ButtonLink> | |
| 425 | </div> | |
| 426 | </Form> | |
| 427 | )} | |
| 428 | </li> | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 429 | ); |
| 430 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.