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 configure
created: 2026-10-15T04:08:53Z
norm: '1'
sig: f3e1bf286b59976d
body_hash: 6b5edb5d1c039626
raw_hash: 5154b99e7146ea7b
vouched: 2026-10-22T14:33:26Z
body_hash: 8ac9e99449a990ef
raw_hash: 5b6fd56a9863703b
vouched: 2026-10-23T12:24:44Z
lines: 46-54
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ id: 01M4YWKAGD3FTMWYWE1KVXT9Q1
anchor: function watch
created: 2026-10-15T04:19:48Z
norm: '1'
sig: 01895e7ff9a47d37
body_hash: fe03a4474929e917
raw_hash: c6d495788240ddb4
vouched: 2026-10-21T21:55:17Z
sig: 576e815f30f4d9cc
body_hash: c1335e94c837e607
raw_hash: a182e4b2f0856f99
vouched: 2026-10-23T12:24:51Z
lines: 89-149
---

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
id: 01M5MBG92EP0H76ECNZM2E28MS
anchor: function configure
created: 2026-10-23T12:24:20Z
norm: '1'
sig: f3e1bf286b59976d
body_hash: 8ac9e99449a990ef
raw_hash: 5b6fd56a9863703b
lines: 85-94
---

every join or leave tells the running host's apps (chose), so their directory views never need a reload
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function serve
created: 2026-10-15T03:39:45Z
norm: '1'
sig: 171797818ef4d836
body_hash: f0a0ef2c205726cc
raw_hash: a98db292f7a9e13d
vouched: 2026-10-22T21:42:37Z
body_hash: bbdacdd140d7e7dd
raw_hash: a8ce4182bc2233cf
vouched: 2026-10-23T12:24:45Z
lines: 668-751
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function serve
created: 2026-10-15T10:17:10Z
norm: '1'
sig: 171797818ef4d836
body_hash: f0a0ef2c205726cc
raw_hash: a98db292f7a9e13d
vouched: 2026-10-22T21:42:37Z
body_hash: bbdacdd140d7e7dd
raw_hash: a8ce4182bc2233cf
vouched: 2026-10-23T12:24:46Z
lines: 830-948
supersedes: 01M4Z2R1HEVDKEA8VEJ7XRJQ8F
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: '@file'
created: 2026-10-20T17:04:56Z
norm: '1'
sig: 1acdc0f6a0895ac5
body_hash: d40dd86c1ca9274b
raw_hash: b0a5d34f855ac98b
vouched: 2026-10-22T22:21:16Z
body_hash: 07a0161028eb0d88
raw_hash: 893db37db5fb95f0
vouched: 2026-10-23T12:24:46Z
lines: 1-1153
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: '@file'
created: 2026-10-22T14:00:47Z
norm: '1'
sig: 1acdc0f6a0895ac5
body_hash: d40dd86c1ca9274b
raw_hash: b0a5d34f855ac98b
vouched: 2026-10-22T22:21:16Z
body_hash: 07a0161028eb0d88
raw_hash: 893db37db5fb95f0
vouched: 2026-10-23T12:24:47Z
lines: 1-1309
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function serve
created: 2026-10-22T21:10:40Z
norm: '1'
sig: 171797818ef4d836
body_hash: f0a0ef2c205726cc
raw_hash: a98db292f7a9e13d
vouched: 2026-10-22T21:42:37Z
body_hash: bbdacdd140d7e7dd
raw_hash: a8ce4182bc2233cf
vouched: 2026-10-23T12:24:49Z
lines: 1191-1341
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ anchor: function serve
created: 2026-10-22T21:42:29Z
norm: '1'
sig: 171797818ef4d836
body_hash: f0a0ef2c205726cc
raw_hash: a98db292f7a9e13d
body_hash: bbdacdd140d7e7dd
raw_hash: a8ce4182bc2233cf
vouched: 2026-10-23T12:24:50Z
lines: 1200-1357
---

Expand Down
8 changes: 5 additions & 3 deletions apps/app/src/directory-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { desktop } from "./desktop";
import { host } from "./host";
import { useDiagnostics } from "./host-settings";
import { parse } from "./invite";
import { CREATE_DIRECTORY, DIRECTORY_CHANGED, People, useRoster } from "./onboarding";
import { CREATE_DIRECTORY, People, useRoster } from "./onboarding";
import { Confirm, Group, Page, Problem, Row, Status, Value, when } from "./settings-layout";
import { DirectoryHosts, TEAM_SETUP } from "./team-setup";

Expand Down Expand Up @@ -233,7 +233,6 @@ function TeamDirectory(
} catch (error) {
setAction({ row, error: (error as Error).message });
}
window.dispatchEvent(new Event(DIRECTORY_CHANGED));
onChange();
}

Expand All @@ -259,7 +258,10 @@ function TeamDirectory(
id="directory-label"
label="Directory"
description={directory.serving
? `Served by this Mac at ${directory.url}`
// `ace directory off` leaves the directory this Mac still serves.
? directory.url
? `Served by this Mac at ${directory.url}`
: "Served by this Mac, which isn't in it"
: directory.url || (
<>
None yet. Teammates see channels only while their Mac is online.{" "}
Expand Down
4 changes: 3 additions & 1 deletion apps/app/src/host-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { Confirm, Group, Page, Problem, Row, Status, Value } from "./settings-la
/** The host's tool and team checks, re-read while `active` and whenever `refresh` is called. */
export function useDiagnostics(active: boolean) {
const status = useSyncExternalStore(host.subscribe, () => host.status);
// Joining or leaving the directory from anywhere, such as the CLI, changes what this shows.
const version = useSyncExternalStore(host.subscribe, () => host.settingsVersion);
const [retry, setRetry] = useState(0);
/** A refresh can also sync the team directory, so its row shows that attempt rather than a stale one. */
const syncing = useRef(false);
Expand All @@ -35,7 +37,7 @@ export function useDiagnostics(active: boolean) {
return () => {
live = false;
};
}, [active, status, retry]);
}, [active, status, version, retry]);

// The shell environment, the first directory sync, and a keychain read waiting on a prompt all
// land in the background; show their results without a manual refresh.
Expand Down
19 changes: 7 additions & 12 deletions apps/app/src/onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,18 @@ import { Checks, Choice, Copyable, Footer, Header, Link, Pill } from "./team-set

const DOWNLOAD = "https://ace.githubnext.com";
const TAILSCALE_USERS = "https://login.tailscale.com/admin/users";
/** Sent after the directory is created, joined, or left, so every view of it reads it again. */
export const DIRECTORY_CHANGED = "ace:directory-changed";
/** Opens Create directory from anywhere: the sidebar card, an offline channel, or Settings. */
export const CREATE_DIRECTORY = "ace:create-directory";

/** The directory as this host last reported it; undefined until the host answers. */
/**
* The directory as this host last reported it; undefined until the host answers. The host tells
* its apps when it joins or leaves one, whether from this app, another window, the CLI, or an
* approved ask.
*/
export function useDirectory(): Diagnostics["directory"] | undefined {
const status = useSyncExternalStore(host.subscribe, () => host.status);
const version = useSyncExternalStore(host.subscribe, () => host.settingsVersion);
const [directory, setDirectory] = useState<Diagnostics["directory"]>();
const [changed, setChanged] = useState(0);
useEffect(() => {
const bump = () => setChanged(value => value + 1);
window.addEventListener(DIRECTORY_CHANGED, bump);
return () => window.removeEventListener(DIRECTORY_CHANGED, bump);
}, []);
useEffect(() => {
if (status !== "open") return;
let live = true;
Expand All @@ -34,7 +31,7 @@ export function useDirectory(): Diagnostics["directory"] | undefined {
return () => {
live = false;
};
}, [status, changed]);
}, [status, version]);
return directory;
}

Expand Down Expand Up @@ -264,7 +261,6 @@ export function DirectoryCard(
setError("");
try {
await host.request({ op: "directory-set", url });
window.dispatchEvent(new Event(DIRECTORY_CHANGED));
} catch (error) {
setError((error as Error).message);
} finally {
Expand Down Expand Up @@ -383,7 +379,6 @@ export function CreateDirectory({ onAdd, onClose }: { onAdd: () => void; onClose
setError("");
try {
setUrl(await host.request<string>({ op: "directory-serve", serve: true }));
window.dispatchEvent(new Event(DIRECTORY_CHANGED));
setStep("done");
} catch (error) {
setError((error as Error).message);
Expand Down
4 changes: 3 additions & 1 deletion apps/app/src/team-setup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -478,9 +478,11 @@ function Nearby({ onJoined, team }: { onJoined: () => void; team?: string }) {
setError("");
try {
await host.request({ op: "team-ask", host: peer.host });
// Joining closes Team setup, so the asker may have looked away; say it outside it.
// Joining closes Team setup, and the asker is often at their teammate's screen while they
// approve, so the news waits outside it until dismissed.
toast.success(`${who} let you in`, {
description: "You're in your team's directory now.",
duration: Infinity,
});
onJoined();
} catch (error) {
Expand Down
11 changes: 8 additions & 3 deletions apps/host/src/directory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ let synced: number | undefined;
let error: string | undefined;
let failed: number | undefined;
/** The running host's watch, so a change made through it takes effect without waiting for a heartbeat. */
let active: { sync(): Promise<void>; again(): Promise<void>; changed(): void } | undefined;
let active:
| { sync(): Promise<void>; again(): Promise<void>; changed(): void; chose(): void }
| undefined;

/** A directory base URL without a trailing slash. */
export function canonical(value: string): string {
Expand All @@ -86,6 +88,7 @@ export function configure(value: string | undefined): Promise<void> | undefined
synced = undefined;
error = undefined;
failed = undefined;
active?.chose();
active?.changed();
return active?.sync();
}
Expand Down Expand Up @@ -262,6 +265,8 @@ type Options = {
channels(): Promise<Listing[]>;
/** After each refresh. */
changed(): void;
/** After joining, changing, or leaving the directory, by any route: the app, the CLI, or a peer's hello. */
chose(): void;
/** After each publish; `before` is this process's previous publication, if any. */
published(before: Listing[] | undefined, after: Listing[]): void;
/** Every project this host knows has a live lobby pushing its changes. */
Expand All @@ -272,7 +277,7 @@ type Options = {
* Publish this host's channels, then refresh what the directory knows. Failures keep the last
* answer, so an unreachable directory never hides channels.
*/
export function watch({ self, channels, changed, published, covered }: Options) {
export function watch({ self, channels, changed, chose, published, covered }: Options) {
const controller = new AbortController();
let running: { base: string; done: Promise<void>; again?: Promise<void> } | undefined;
let before: Listing[] | undefined;
Expand Down Expand Up @@ -332,7 +337,7 @@ export function watch({ self, channels, changed, published, covered }: Options)
if (!running || running.base !== url()) return join();
return running.again ??= running.done.then(join);
};
active = { sync: join, again: sync, changed };
active = { sync: join, again: sync, changed, chose };
void join();
const interval = setInterval(() => {
// A failed publish keeps retrying every heartbeat; lobbies only carry what was published.
Expand Down
1 change: 1 addition & 0 deletions apps/host/src/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1279,6 +1279,7 @@ export async function serve(port: number, loopback = false, script?: string): Pr
self: () => ({ name, login: catalog.user, github: github.login(loginChanged), ...address }),
channels: local,
changed: () => void sharesChanged(),
chose: settingsChanged,
published: (before, after) => before && lobby.published(before, after),
covered: lobby.isCovered,
});
Expand Down
5 changes: 4 additions & 1 deletion apps/host/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,10 @@ export type HostFrame =
/** What a pending request waits on, such as keychain access; null once it no longer waits. */
| { id: number; waiting: string | null }
| { rename: TabRename }
/** Local settings changed; clients reload model availability without receiving secrets. */
/**
* Local settings or the joined directory changed; clients reload model availability and the
* directory's state without receiving secrets.
*/
| { settings: true }
/** Opened projects belong to the local owner and are never sent to tailnet peers. */
| { projects: Project[] }
Expand Down