Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function App
created: 2026-10-15T03:38:04Z
norm: '1'
sig: 063d0f00fea02511
body_hash: e05dde251a7f4b1a
raw_hash: f16de82a81e3c77d
vouched: 2026-10-22T15:24:24Z
body_hash: 70b144740681d82c
raw_hash: 2962395e6c4e618a
vouched: 2026-10-22T15:45:21Z
lines: 235-824
---

Expand Down
9 changes: 5 additions & 4 deletions apps/app/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ import {
useDirectory,
Welcome,
} from "./onboarding";
import { TeamSetup } from "./team-setup";
import { TEAM_SETUP, TeamSetup, TeamSetupItem } from "./team-setup";
import { UpdateNotice } from "./updates";
import { useWindowStorage } from "./window-storage";

Expand Down Expand Up @@ -395,7 +395,7 @@ export function App() {
const add = () => setTeammate(true);
window.addEventListener(CREATE_DIRECTORY, create);
window.addEventListener("ace:add-teammate", add);
window.addEventListener("ace:team-setup", open);
window.addEventListener(TEAM_SETUP, open);
window.addEventListener("ace:team-invite", invite);
window.addEventListener("ace:join", joining);
const fragment = new URLSearchParams(location.hash.slice(1)).get("join");
Expand All @@ -406,7 +406,7 @@ export function App() {
return () => {
window.removeEventListener(CREATE_DIRECTORY, create);
window.removeEventListener("ace:add-teammate", add);
window.removeEventListener("ace:team-setup", open);
window.removeEventListener(TEAM_SETUP, open);
window.removeEventListener("ace:team-invite", invite);
window.removeEventListener("ace:join", joining);
};
Expand Down Expand Up @@ -858,7 +858,7 @@ export function App() {
side="left"
id="sidebar"
className="-my-2 h-[calc(100%+1rem)]"
innerClassName={card ? "flex h-full min-h-0 flex-col" : "h-full min-h-0"}
innerClassName="flex h-full min-h-0 flex-col"
>
{page === "channels"
? (
Expand Down Expand Up @@ -947,6 +947,7 @@ export function App() {
/>
)}
{card && <DirectoryCard onDismiss={() => setDismissed(true)} />}
<TeamSetupItem />
</Sidebar>
)}
<Main
Expand Down
9 changes: 9 additions & 0 deletions apps/app/src/directory-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useDiagnostics } from "./host-settings";
import { parse } from "./invite";
import { CREATE_DIRECTORY, DIRECTORY_CHANGED, People, useRoster } from "./onboarding";
import { Confirm, Group, Page, Problem, Row, Status, Value } from "./settings-layout";
import { TEAM_SETUP } from "./team-setup";

const SETUP = "https://github.com/githubnext/ace/blob/main/docs/setup.md#team-directory";

Expand Down Expand Up @@ -95,6 +96,14 @@ export function TeamSettings({ active }: { active: boolean }) {
)}
</Row>
<TeamDirectory directory={checked.directory} onChange={() => checks.refresh()} />
<Row
label="Team setup"
description="Ask a teammate's host to let you in, or deploy a directory to Cloudflare or Fly.io."
>
<Button variant="outline" onClick={() => window.dispatchEvent(new Event(TEAM_SETUP))}>
Set up…
</Button>
</Row>
</>
)}
</Group>
Expand Down
17 changes: 16 additions & 1 deletion apps/app/src/team-setup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ import {
DialogTitle,
Input,
} from "@ace/ui";
import { IconArrowUpRight, IconCheck, IconCopy, IconLoader } from "@ace/ui/icons";
import { IconArrowUpRight, IconCheck, IconCopy, IconLoader, IconUsers } from "@ace/ui/icons";
import type { TeamPeer } from "@ace/host/protocol";

import { host } from "./host";
import { useDiagnostics } from "./host-settings";
import { type Invite as Team, link, page, parse } from "./invite";

const DOCS = "https://github.com/githubnext/ace/blob/main/docs/setup.md";
export const TEAM_SETUP = "ace:team-setup";
const PROVIDERS = {
cloudflare: { label: "Cloudflare" },
fly: { label: "Fly.io" },
Expand All @@ -40,6 +41,20 @@ const STEPS: Partial<Record<Step, string>> = {
* Membership stays the tailnet's: this only decides where the team's channels are listed.
* `invite` opens on joining with an invite link's values; `inviting` opens on this team's invite.
*/
/** Sidebar footer entry to team setup, where Ask to join and deploying a directory live. */
export function TeamSetupItem() {
return (
<button
type="button"
onClick={() => window.dispatchEvent(new Event(TEAM_SETUP))}
className="mx-2 mb-2 flex h-7 shrink-0 items-center gap-2 rounded-md px-2 text-xs font-medium text-muted-foreground outline-none transition-colors hover:bg-foreground/10 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/30"
>
<IconUsers className="size-3.5" aria-hidden />
Team setup
</button>
);
}

export function TeamSetup({ github, invite, inviting, onClose }: {
github?: string;
invite?: Team;
Expand Down
10 changes: 7 additions & 3 deletions docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ tailnet finds it in this host's hello and joins it on its own, unless it left a

Teams that set up a directory before can keep it deployed as a cell. Whoever sets up the team
deploys the services once from a checkout; everyone else joins in `Settings → Team` (Join…) with
the directory's URL and the team's secret, shared privately. Refresh there syncs at once. From a
the directory's URL and the team's secret, shared privately, or uses **Ask to join** next to a
teammate's host in **Team setup** (at the bottom of the sidebar, or `Settings → Team → Set up…`),
which their owner approves in their Ace. Refresh in `Settings → Team` syncs at once. From a
checkout, `printf %s "$SECRET" | bun ace key set ACE_SECRET` then `bun ace directory <url>` does
the same.

Expand Down Expand Up @@ -107,8 +109,10 @@ printf %s https://ace-app.<your-subdomain>.workers.dev | bunx wrangler secret pu

### From the app

**Get your team started** (also under `Settings → Team`) can deploy to Cloudflare for you, with
no checkout. **Deploy** does this, and nothing else:
**Team setup**, at the bottom of the sidebar or `Settings → Team → Set up…`, can deploy to
Cloudflare for you, with no checkout: choose **Get your team started**, then a hosted directory on
Cloudflare. For Fly.io it shows the command to run from a checkout and joins with what it prints.
**Deploy** does this, and nothing else:

- Signs in with your own `wrangler login`. If you aren't signed in, it opens Cloudflare's sign-in
page in your browser. Ace never sees a Cloudflare password or token.
Expand Down