Files
PaperClipAI/ui/src/lib/cloudLinks.ts
T
Devin FoleyandPaperclip c341588bdb fix(ui): add Cloud invitations to the Members page (#13922)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - People manage collaborators from the Members page.
> - Cloud manages invitations outside the tenant's local invitation
system.
> - The local Invites tab is hidden on Cloud, so this page has no way to
invite a person.
> - This pull request adds an Invite people action for the current Cloud
stack's owner or admin.
> - The action opens the existing Cloud People settings for that stack.

## Linked Issues or Issue Description

**What happened?**

A Cloud owner opens Organization Settings → Members and finds no
invitation action. The tenant-local Invites tab is hidden, and the page
does not link to Cloud's invitation flow.

**Expected behavior**

Cloud owners and admins can start an invitation from Members.

**Steps to reproduce**

1. Sign in to a Cloud-managed instance as the current stack's owner or
admin.
2. Open Organization Settings → Members with `company.invites` hidden.
3. Look for an invitation action beside the page heading.

**Paperclip version or commit**

`7b7c4d4172d6aac14919e2682b702ae87bc17653`.

**Deployment mode**

Cloud-managed, authenticated.

Related search: #2388 proposes broader member-management UI. This change
only connects the existing Members page to Cloud invitations. No
duplicate Cloud invitation action PR was found.

## What Changed

- Add **Invite people** beside the Members heading for the current Cloud
stack's owner/admin.
- Read the role from the authenticated Cloud portfolio. Ownership of
another stack does not enable the action.
- Navigate to the current stack's People settings on the configured
Cloud origin. Keep the local Invites tab hidden when configured.
- Cover allowed roles, denied roles, loading, failed refresh, missing
configuration, current-stack selection, and self-hosted behavior.
Document the navigation contract.

## Verification

- Focused Members and Cloud link tests: 23 passed.
- Full UI suite: 6,669 passed across 634 files.
- UI typecheck and `pnpm check:token-gates`: passed.
- `pnpm build`: passed.
- `pnpm -r typecheck`: passed.
- All PR CI checks passed, including the full general, serialized,
browser, and runner test jobs. The duplicate local repository-wide `pnpm
test:run` was stopped after CI completed; it is not reported as a local
pass.
- Manual acceptance after tenant rollout: an owner/admin opens Members,
selects **Invite people**, and reaches the same stack's Cloud People
settings. A member does not see the action.

## Risks

- The action needs a tenant app update before it appears on an existing
stack.
- The portfolio request must identify the current stack and its role.
The action stays hidden when that information is unavailable or the
request fails.
- Cloud rechecks invitation authorization at the destination. No schema,
API, or invitation-acceptance behavior changes.

## Model Used

- OpenAI Codex, GPT-6, with reasoning, code execution, and repository
tools. The session does not expose a more specific model identifier or
context-window size.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-09-24 08:48:04 -07:00

66 lines
2.6 KiB
TypeScript

/**
* Links out of a tenant instance and into the Paperclip Cloud app.
*
* The base always comes from the health `cloud` block (derived from
* `PAPERCLIP_CLOUD_API_ORIGIN`) — the cloud domain is never hardcoded, because
* aliases move and self-hosted instances have no cloud block at all. Only the
* origin of that value is used; a control-plane path suffix is dropped, exactly
* as the server-side portfolio proxy resolves it.
*
* These are always full top-level navigations, never in-app routes: the cloud
* harness shadows GET `/stacks` and `/stacks/*` on tenant hosts, so an in-app
* route under that prefix would be unreachable.
*/
export function cloudAppUrl(cloudBaseUrl: string | null | undefined, path: string): string | null {
const base = cloudBaseUrl?.trim();
if (!base) return null;
try {
const url = new URL(path, base);
// The base is server-supplied, but a stray non-web scheme must never reach
// `window.location.assign`.
if (url.protocol !== "https:" && url.protocol !== "http:") return null;
return url.toString();
} catch {
return null;
}
}
/**
* Entry-code handoff for another stack: cloud authenticates the user for that
* stack and wakes it if it is asleep before redirecting to its tenant host.
*/
export function cloudStackEnterUrl(
cloudBaseUrl: string | null | undefined,
stackSlug: string | null | undefined,
): string | null {
const slug = stackSlug?.trim();
if (!slug) return null;
return cloudAppUrl(cloudBaseUrl, `/stacks/${encodeURIComponent(slug)}/enter`);
}
/** Cloud's own create-a-stack flow, which replaces the in-app company wizard. */
export function cloudStackCreateUrl(cloudBaseUrl: string | null | undefined): string | null {
return cloudAppUrl(cloudBaseUrl, "/stacks/new");
}
/** Cloud manages human invitations in the current stack's People settings. */
export function cloudStackInviteUrl(
cloudBaseUrl: string | null | undefined,
stackSlug: string | null | undefined,
): string | null {
const slug = stackSlug?.trim();
if (!slug) return null;
return cloudAppUrl(cloudBaseUrl, `/workspaces/${encodeURIComponent(slug)}/settings?section=people`);
}
/**
* Cloud's organization portfolio in its explicit manage view. `?manage=1`
* matters: the plain launchpad auto-forwards a solo user straight back into
* their one openable stack, and the caller here has just archived that
* stack's only company — bouncing back into it is exactly what the
* navigation is escaping.
*/
export function cloudPortfolioManageUrl(cloudBaseUrl: string | null | undefined): string | null {
return cloudAppUrl(cloudBaseUrl, "/orgs?manage=1");
}