From 659978bf4e39273f4dbbf1c7f60b57f4a6e211f6 Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Fri, 23 Oct 2026 11:16:47 -0400 Subject: [PATCH 1/3] fix(app): keep a teammate's name and order in offline sections, and return Team setup to Settings Why: apps/app/src/projects.ts#function projects | projects follow channel creation order, not the host's listing order, which moves an offline peer's listings behind the directory's --- apps/app/src/app.tsx | 9 +++++++-- apps/app/src/directory-settings.tsx | 6 +++++- apps/app/src/projects.ts | 5 ++++- apps/app/src/team-setup.tsx | 11 +++++++---- .../components/session-sidebar/session-sidebar.tsx | 10 +++++++++- 5 files changed, 32 insertions(+), 9 deletions(-) diff --git a/apps/app/src/app.tsx b/apps/app/src/app.tsx index f71ea593..8d328d26 100644 --- a/apps/app/src/app.tsx +++ b/apps/app/src/app.tsx @@ -368,6 +368,7 @@ export function App() { const [offered, setOffered] = useLocalStorage("ace:team-setup", false); // Joining or inviting to a deployed directory, which needs the team secret. const [teaming, setTeaming] = useState(false); + const [returning, setReturning] = useState
(); const [directoryDialog, setDirectoryDialog] = useState(false); const [teammate, setTeammate] = useState(false); const [dismissed, setDismissed] = useLocalStorage("ace:directory-card", false); @@ -400,9 +401,11 @@ export function App() { return () => listeners.forEach((remove) => remove()); }, []); useEffect(() => { - const open = () => { + // Opened from a Settings section, closing Team setup returns there. + const open = (event: Event) => { setSettings(false); setTeaming(true); + setReturning((event as CustomEvent
).detail); }; const invite = () => { setSettings(false); @@ -613,7 +616,8 @@ export function App() { const group: SessionSidebarGroupId = `${id}:team:${owner}`; return { id: group, - label: `${name} · ${teammate}`, + label: name, + owner: teammate, note: offline(rows), rows, sort: "none", @@ -1344,6 +1348,7 @@ export function App() { onClose={() => { setOffered(true); setTeaming(false); + if (returning) setSettings(returning); }} /> )} diff --git a/apps/app/src/directory-settings.tsx b/apps/app/src/directory-settings.tsx index 846c4d7a..63372f6c 100644 --- a/apps/app/src/directory-settings.tsx +++ b/apps/app/src/directory-settings.tsx @@ -100,7 +100,11 @@ export function TeamSettings({ active }: { active: boolean }) { label="Team setup" description="Ask a teammate's host to let you in, or deploy a directory to Cloudflare or Fly.io." > - diff --git a/apps/app/src/projects.ts b/apps/app/src/projects.ts index 7702620e..2f3a1713 100644 --- a/apps/app/src/projects.ts +++ b/apps/app/src/projects.ts @@ -105,7 +105,10 @@ export function projects(opened: Project[], channels: Listing[], host: string): add(host, project.path, project.name, true, project.repo, project); } } - for (const channel of channels) { + // Creation order, not the host's: a peer's listings move behind the directory's while it is + // offline, which would re-sort projects known only through its channels. + const ordered = [...channels].sort((a, b) => a.created - b.created || a.id.localeCompare(b.id)); + for (const channel of ordered) { const path = root(channel); const name = channel.repo?.split("/")[1] || path.replace(/\/+$/, "").split("/").pop() || path; // Directory-only listings are offline or archived, and a hosted channel's host is a workspace. diff --git a/apps/app/src/team-setup.tsx b/apps/app/src/team-setup.tsx index 70aa6746..1b49ff87 100644 --- a/apps/app/src/team-setup.tsx +++ b/apps/app/src/team-setup.tsx @@ -76,7 +76,9 @@ export function TeamSetup({ github, invite, inviting, onClose }: { // Until the host answers, the fork can't tell a first person from one already in a team. const pending = checks.open && !checked && !checks.error; const peers = useTeamPeers(); - const starting = chosen ?? (!checked?.directory.offered && !peers.length); + // Someone on the tailnet is already in a team, so this person isn't the first. + const team = !!checked?.directory.offered || peers.length > 0; + const starting = chosen ?? !team; useEffect(() => { if (!inviting) return; @@ -126,10 +128,11 @@ export function TeamSetup({ github, invite, inviting, onClose }: { setStarting(true)} - title="Get your team started" + title={team ? "Set up a separate directory" : "Get your team started"} > - You're the first on this tailnet. Pick where the team's channels are listed. You can - change it later. + {team + ? "Your tailnet already has a team. Only list your channels apart from theirs if you mean to." + : "You're the first on this tailnet. Pick where the team's channels are listed. You can change it later."} + group.owner ? `${group.label} · ${group.owner}` : group.label; + function GroupHeaderContent({ group, collapsed }: { group: SessionSidebarGroup; collapsed: boolean; @@ -392,6 +398,7 @@ function GroupHeaderContent({ group, collapsed }: { )} {group.label} + {group.owner && {` · ${group.owner}`}} {group.note && {` · ${group.note}`}}