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: d83ccb35867e607a
raw_hash: 90ddf2e883ff2d5c
vouched: 2026-10-23T14:03:54Z
body_hash: 01c00d19aa6a1d10
raw_hash: 5464f979a3f6de84
vouched: 2026-10-23T15:16:53Z
lines: 235-824
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function App
created: 2026-10-22T20:29:16Z
norm: '1'
sig: 063d0f00fea02511
body_hash: d83ccb35867e607a
raw_hash: 90ddf2e883ff2d5c
vouched: 2026-10-23T14:03:54Z
body_hash: 01c00d19aa6a1d10
raw_hash: 5464f979a3f6de84
vouched: 2026-10-23T15:16:54Z
lines: 330-1237
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function App
created: 2026-10-23T14:03:19Z
norm: '1'
sig: 063d0f00fea02511
body_hash: d83ccb35867e607a
raw_hash: 90ddf2e883ff2d5c
vouched: 2026-10-23T14:03:54Z
body_hash: 01c00d19aa6a1d10
raw_hash: 5464f979a3f6de84
vouched: 2026-10-23T15:16:54Z
lines: 334-1357
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function projects
created: 2026-10-15T02:51:27Z
norm: '1'
sig: a2ec110fa31d81ee
body_hash: 61e3738b3ac3c6ad
raw_hash: edf6c307fa9849bb
vouched: 2026-10-22T22:08:11Z
body_hash: b2cb815c82d2f660
raw_hash: 6939b16ea6db7a14
vouched: 2026-10-23T15:16:54Z
lines: 42-91
---

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
id: 01M5MNC23AYSNE0E4FR7HXW7FF
anchor: function projects
created: 2026-10-23T15:16:47Z
norm: '1'
sig: a2ec110fa31d81ee
body_hash: b2cb815c82d2f660
raw_hash: 6939b16ea6db7a14
lines: 65-122
---

projects follow channel creation order, not the host's listing order, which moves an offline peer's listings behind the directory's
9 changes: 7 additions & 2 deletions apps/app/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean | "inviting" | Invite>(false);
const [returning, setReturning] = useState<Section>();
const [directoryDialog, setDirectoryDialog] = useState(false);
const [teammate, setTeammate] = useState(false);
const [dismissed, setDismissed] = useLocalStorage("ace:directory-card", false);
Expand Down Expand Up @@ -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<Section | undefined>).detail);
};
const invite = () => {
setSettings(false);
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -1344,6 +1348,7 @@ export function App() {
onClose={() => {
setOffered(true);
setTeaming(false);
if (returning) setSettings(returning);
}}
/>
)}
Expand Down
6 changes: 5 additions & 1 deletion apps/app/src/directory-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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."
>
<Button variant="outline" onClick={() => window.dispatchEvent(new Event(TEAM_SETUP))}>
<Button
variant="outline"
onClick={() =>
window.dispatchEvent(new CustomEvent(TEAM_SETUP, { detail: "team" }))}
>
Set up…
</Button>
</Row>
Expand Down
5 changes: 4 additions & 1 deletion apps/app/src/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 7 additions & 4 deletions apps/app/src/team-setup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -126,10 +128,11 @@ export function TeamSetup({ github, invite, inviting, onClose }: {
<Choice
selected={starting}
onSelect={() => 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."}
</Choice>
<Choice
selected={!starting}
Expand Down
10 changes: 9 additions & 1 deletion packages/ui/src/components/session-sidebar/session-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ export type SessionSidebarGroupId =
export type SessionSidebarGroup = {
id: SessionSidebarGroupId;
label: string;
/** A teammate who owns the group's channels; the label shortens first so it stays visible. */
owner?: string;
/** Follows the label and is never truncated with it, such as Offline. */
note?: string;
rows: SidebarRow[];
Expand Down Expand Up @@ -376,6 +378,10 @@ export function ProjectPicker(
);
}

/** The group's whole name, for hover and screen readers when the header shortens it. */
const name = (group: SessionSidebarGroup) =>
group.owner ? `${group.label} · ${group.owner}` : group.label;

function GroupHeaderContent({ group, collapsed }: {
group: SessionSidebarGroup;
collapsed: boolean;
Expand All @@ -392,6 +398,7 @@ function GroupHeaderContent({ group, collapsed }: {
)}
<span className="flex min-w-0">
<span className="truncate">{group.label}</span>
{group.owner && <span className="shrink-0 whitespace-pre">{` · ${group.owner}`}</span>}
{group.note && <span className="shrink-0 whitespace-pre">{` · ${group.note}`}</span>}
</span>
<motion.span
Expand Down Expand Up @@ -643,9 +650,10 @@ function Group({
>
<button
type="button"
aria-label={`${collapsed ? "Expand" : "Collapse"} ${group.label}${
aria-label={`${collapsed ? "Expand" : "Collapse"} ${name(group)}${
group.note ? `, ${group.note}` : ""
}`}
title={[name(group), group.note].filter(Boolean).join(" · ")}
aria-expanded={!collapsed}
aria-controls={content}
onClick={() => onToggleGroup?.(group.id)}
Expand Down