refactor(web,desktop): Linear becomes the first of provider-keyed integrations (integrations 1/5)

Merged
DA
7d ago
+8437-2407

What

Dean: "Let's add Notion, Slack, Sentry, Google Docs/Drive. And let's pull out the settings to a new Integrations tab." This is the first PR of that rollout. It turns the Linear connection into the first of several provider-keyed integrations, and Linear keeps working exactly as before. It also does Dean's follow-up: "clean up the confirmation page for Linear connected (and any other services). It should show the same as successful sign in with GitHub. And don't have so much text in the primary or subtext."

  1. One provider table. apps/web/server/hosted/integrations.ts gives each provider:

    • the env variable its Vercel Connect connector UID is read from;
    • its hosted MCP server and what the model is told about it;
    • the fixed name its nested calls are stored under in a transcript.

    The shared vocabulary is packages/hosted/src/integration-wire.ts: INTEGRATION_PROVIDER (as const), the statuses, and the landing statuses. Each later provider PR adds one entry, four one-line eve connection files, a mark, and a privacy paragraph.

  2. Store. A new integration_connection table keyed (user_id, provider) (migration 0096). The migration copies every linear_connection row in as Linear's, so a connected account stays connected and a sign-in under way still settles. linear_connection is no longer read. It stays for one deploy, so the build still serving during this deploy keeps working, and the next PR's migration drops it.

  3. Routes. One group (server/integrations-app.ts) replaces the four Linear routes, and the desktop moves to it in the same PR, since no desktop has shipped:

    • GET /api/account/integrations answers { integrations: { linear: "connected" | … } }.
    • POST /api/account/integrations/{provider}/connect.
    • POST /api/account/integrations/{provider}/disconnect.
    • GET /api/account/integrations/connected?state=… is the one Connect callback for every provider; the state names both the account and the provider.

    The rewrite moves {provider} into a provider query, the way a plan's id is moved. An unknown provider gets 404.

  4. Agents. Every agent registers each provider's connection through one factory, integrationConnection(provider, read) in server/hosted/integration-connection.ts. Each connections/linear.ts (eve, coder, and both workers) is still one line. Sentences, admission, the token read, and the "a Disconnect wins over an in-flight token" behaviour are unchanged.

  5. Desktop.

    • Gateway methods and acts are now integrations.status / integrations.connect {provider} / integrations.disconnect {provider}, through HostedIntegrationsClient and compose-integrations.ts.
    • IntegrationRow takes a provider, and useIntegrationStatuses() reads every provider once.
    • Linear's row is drawn where it was (Settings › General). Moving it to Settings › Integrations is the next PR.
    • The sign-in address is still opened by the host and never reaches a window.
  6. Account deletion revokes the grant at every configured provider at Vercel Connect. This generalizes #2299's revoke (revokeIntegrationGrants), and PRIVACY.md keeps #2299's wording.

  7. The connected page (integration-connected.html, formerly linear-connected.html) now dresses like the page a GitHub sign-in leaves the browser on: the provider's mark, an arrow, Luke's face, one short line, and one shorter one. "Linear connected / You can return to Luke." Each failure is just as short, e.g. "Linear not connected / Return to Luke to try again." The pairing graphic, previously duplicated in sign-in.tsx and github-installed.tsx, is now one connection-graphic.tsx. Linear's mark path data comes from design/generate-surface-shared.mjs (Simple Icons' trace, CC0), next to GitHub's and Google's.

Tests

  • integrations-app.test.ts: the route goldens, moved and re-recorded under LUKE_UPDATE_FIXTURES=1 in fixtures/integrations-route/. The diff renames paths and nests status under integrations. New cases: an unknown provider and a missing provider are 404.
  • integration-store.test.ts (PGlite): a sign-in is found by its state, completes once, and expires only when connected. One account never reads another's rows, and the rows go with the account.
  • integration-migration.test.ts: rows written to linear_connection before 0096 read back connected / pending-by-state after it. It fails without the copy statement.
  • integration-connection.test.ts, integration-agents.test.ts, linear-transcript.test.ts: ported unchanged in behaviour.
  • account-app.test.ts: a delete revokes every provider's grant; a failed revoke still deletes; a deployment with no connector asks Connect nothing.
  • compose-integrations.test.ts: an unknown provider is refused unasked.
  • integration-client.test.ts: a provider missing from the answer is no answer.
  • integration-acts.test.ts, integration-row.test.ts: ported to the provider-keyed acts.

./scripts/check.sh exits 0.

Screenshots

These are rendered in headless Chrome on Linux. The GitHub page is the desktop's loopback page (accountLoopbackPage), rendered with the strings a GitHub sign-in gives it.

GitHub sign-in (existing)Linear connected (new)
githublinear connected
Not completedExpired link (no provider known)
not completedexpired

Mac validation

CI could not verify the Mac UI: no CI job builds the Mac app. ./scripts/verify.sh was not run, because this is a Linux cloud VM; LUKE-159 is the manual Mac pass. The desktop changes here are wiring only, and the Settings row looks the same.

🤖 Generated with Claude Code